mirror of
https://github.com/discourse/discourse.git
synced 2025-02-25 18:55:32 -06:00
- FEATURE: revamped poll plugin
- add User.staff scope - inject MessageBus into Ember views (so it can be used by the poll plugin) - REFACTOR: use more accurate is_first_post? method instead of post_number == 1 - FEATURE: add support for JSON-typed custom fields - FEATURE: allow plugins to add validation - FEATURE: add post_custom_fields to PostSerializer - FEATURE: allow plugins to whitelist post_custom_fields - FIX: don't bump when post did not save successfully - FEATURE: polls are supported in any post - FEATURE: allow for multiple polls in the same post - FEATURE: multiple choice polls - FEATURE: rating polls - FEATURE: new dialect allowing users to preview polls in the composer
This commit is contained in:
@@ -1,39 +0,0 @@
|
||||
# Poll plugin
|
||||
|
||||
Allows you to add a poll to the first post of a topic.
|
||||
|
||||
# Usage
|
||||
|
||||
1. Make your topic title start with "Poll: "
|
||||
2. Include a list in your post (the **first list** will be used)
|
||||
|
||||
Important note:
|
||||
|
||||
Make sure you have the "Poll: " prefix in the title right from the start.
|
||||
Editing the title to include it later is not possible atm.
|
||||
|
||||
## Closing the poll
|
||||
|
||||
Change the start of the topic title from "Poll: " to "Closed Poll: ". This feature uses the locale of the user who started the topic.
|
||||
|
||||
_Note: closing a topic will also close the poll._
|
||||
|
||||
## Specifying the list to be used for the poll
|
||||
|
||||
If you have multiple lists in your post and the first list is _not_
|
||||
the one you want to use for the poll, you can identify the
|
||||
list to be used like this:
|
||||
|
||||
```
|
||||
Intro Text
|
||||
|
||||
- Item one
|
||||
- Item two
|
||||
|
||||
Here are your choices:
|
||||
|
||||
[poll]
|
||||
- Option 1
|
||||
- Option 2
|
||||
[/poll]
|
||||
```
|
||||
@@ -0,0 +1,28 @@
|
||||
export default Em.Component.extend({
|
||||
tagName: "li",
|
||||
attributeBindings: ["data-poll-option-id", "data-poll-selected", "style"],
|
||||
|
||||
"data-poll-option-id": Em.computed.alias("option.id"),
|
||||
|
||||
"data-poll-selected": function() {
|
||||
return this.get("option.selected") ? "selected" : false;
|
||||
}.property("option.selected"),
|
||||
|
||||
style: function() {
|
||||
var styles = [];
|
||||
if (this.get("color")) { styles.push("color:" + this.get("color")); }
|
||||
if (this.get("background")) { styles.push("background:" + this.get("background")); }
|
||||
return styles.length > 0 ? styles.join(";") : false;
|
||||
}.property("color", "background"),
|
||||
|
||||
render(buffer) {
|
||||
buffer.push(this.get("option.html"));
|
||||
},
|
||||
|
||||
click(e) {
|
||||
// ensure we're not clicking on a link
|
||||
if ($(e.target).closest("a").length === 0) {
|
||||
this.sendAction("toggle", this.get("option"));
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,22 @@
|
||||
import round from "discourse/plugins/poll/lib/round";
|
||||
|
||||
export default Em.Component.extend({
|
||||
tagName: "span",
|
||||
|
||||
totalScore: function() {
|
||||
return _.reduce(this.get("poll.options"), function(total, o) {
|
||||
const value = parseInt(o.get("html"), 10),
|
||||
votes = parseInt(o.get("votes"), 10);
|
||||
return total + value * votes;
|
||||
}, 0);
|
||||
}.property("poll.options.@each.{html,votes}"),
|
||||
|
||||
average: function() {
|
||||
return round(this.get("totalScore") / this.get("poll.total_votes"), -2);
|
||||
}.property("totalScore", "poll.total_votes"),
|
||||
|
||||
averageRating: function() {
|
||||
return I18n.t("poll.average_rating", { average: this.get("average") });
|
||||
}.property("average"),
|
||||
|
||||
});
|
||||
@@ -0,0 +1,25 @@
|
||||
export default Em.Component.extend({
|
||||
tagName: "table",
|
||||
classNames: ["results"],
|
||||
|
||||
options: function() {
|
||||
const totalVotes = this.get("poll.total_votes"),
|
||||
backgroundColor = this.get("poll.background");
|
||||
|
||||
this.get("poll.options").forEach(option => {
|
||||
const percentage = Math.floor(100 * option.get("votes") / totalVotes),
|
||||
styles = ["width: " + percentage + "%"];
|
||||
|
||||
if (backgroundColor) { styles.push("background: " + backgroundColor); }
|
||||
|
||||
option.setProperties({
|
||||
percentage: percentage,
|
||||
title: I18n.t("poll.option_title", { count: option.get("votes") }),
|
||||
style: styles.join(";")
|
||||
});
|
||||
});
|
||||
|
||||
return this.get("poll.options");
|
||||
}.property("poll.total_votes", "poll.options.[]")
|
||||
|
||||
});
|
||||
@@ -1,48 +1,188 @@
|
||||
import DiscourseController from 'discourse/controllers/controller';
|
||||
export default Em.Controller.extend({
|
||||
isMultiple: Em.computed.equal("poll.type", "multiple"),
|
||||
isNumber: Em.computed.equal("poll.type", "number"),
|
||||
isRandom : Em.computed.equal("poll.order", "random"),
|
||||
isClosed: Em.computed.equal("poll.status", "closed"),
|
||||
|
||||
export default DiscourseController.extend({
|
||||
poll: null,
|
||||
showResults: Em.computed.oneWay('poll.closed'),
|
||||
disableRadio: Em.computed.any('poll.closed', 'loading'),
|
||||
showToggleClosePoll: Em.computed.alias('poll.post.topic.details.can_edit'),
|
||||
// immediately shows the results when the user has already voted
|
||||
showResults: Em.computed.gt("vote.length", 0),
|
||||
|
||||
// shows the results when
|
||||
// - poll is closed
|
||||
// - topic is archived/closed
|
||||
// - user wants to see the results
|
||||
showingResults: Em.computed.or("isClosed", "post.topic.closed", "post.topic.archived", "showResults"),
|
||||
|
||||
showResultsDisabled: Em.computed.equal("poll.total_votes", 0),
|
||||
hideResultsDisabled: Em.computed.alias("isClosed"),
|
||||
|
||||
poll: function() {
|
||||
const poll = this.get("model"),
|
||||
vote = this.get("vote");
|
||||
|
||||
if (poll) {
|
||||
const options = _.map(poll.get("options"), o => Em.Object.create(o));
|
||||
|
||||
if (vote) {
|
||||
options.forEach(o => o.set("selected", vote.indexOf(o.get("id")) >= 0));
|
||||
}
|
||||
|
||||
poll.set("options", options);
|
||||
}
|
||||
|
||||
return poll;
|
||||
}.property("model"),
|
||||
|
||||
selectedOptions: function() {
|
||||
return _.map(this.get("poll.options").filterBy("selected"), o => o.get("id"));
|
||||
}.property("poll.options.@each.selected"),
|
||||
|
||||
totalVotesText: function() {
|
||||
return I18n.t("poll.total_votes", { count: this.get("poll.total_votes") });
|
||||
}.property("poll.total_votes"),
|
||||
|
||||
min: function() {
|
||||
let min = parseInt(this.get("poll.min"), 10);
|
||||
if (isNaN(min) || min < 1) { min = 1; }
|
||||
return min;
|
||||
}.property("poll.min"),
|
||||
|
||||
max: function() {
|
||||
let options = this.get("poll.options.length"),
|
||||
max = parseInt(this.get("poll.max"), 10);
|
||||
if (isNaN(max) || max > options) { max = options; }
|
||||
return max;
|
||||
}.property("poll.max", "poll.options.length"),
|
||||
|
||||
multipleHelpText: function() {
|
||||
const options = this.get("poll.options.length"),
|
||||
min = this.get("min"),
|
||||
max = this.get("max");
|
||||
|
||||
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: min, max: 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 });
|
||||
}
|
||||
}
|
||||
}.property("min", "max", "poll.options.length"),
|
||||
|
||||
canCastVotes: function() {
|
||||
if (this.get("isClosed") ||
|
||||
this.get("showingResults") ||
|
||||
this.get("loading")) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const selectedOptionCount = this.get("selectedOptions.length");
|
||||
|
||||
if (this.get("isMultiple")) {
|
||||
return selectedOptionCount >= this.get("min") && selectedOptionCount <= this.get("max");
|
||||
} else {
|
||||
return selectedOptionCount > 0;
|
||||
}
|
||||
}.property("isClosed", "showingResults", "loading",
|
||||
"selectedOptions.length",
|
||||
"isMultiple", "min", "max"),
|
||||
|
||||
castVotesDisabled: Em.computed.not("canCastVotes"),
|
||||
|
||||
canToggleStatus: function() {
|
||||
return this.currentUser &&
|
||||
(this.currentUser.get("id") === this.get("post.user_id") || this.currentUser.get("staff")) &&
|
||||
!this.get("loading") &&
|
||||
!this.get("post.topic.closed") &&
|
||||
!this.get("post.topic.archived");
|
||||
}.property("loading", "post.user_id", "post.topic.{closed,archived}"),
|
||||
|
||||
actions: {
|
||||
selectOption(option) {
|
||||
if (this.get('disableRadio')) {
|
||||
return;
|
||||
|
||||
toggleOption(option) {
|
||||
if (this.get("isClosed")) { return; }
|
||||
if (!this.currentUser) { return this.send("showLogin"); }
|
||||
|
||||
const wasSelected = option.get("selected");
|
||||
|
||||
if (!this.get("isMultiple")) {
|
||||
this.get("poll.options").forEach(o => o.set("selected", false));
|
||||
}
|
||||
|
||||
if (!this.get('postController.currentUser.id')) {
|
||||
this.get('postController').send('showLogin');
|
||||
return;
|
||||
}
|
||||
option.toggleProperty("selected");
|
||||
|
||||
this.set('loading', true);
|
||||
if (!this.get("isMultiple") && !wasSelected) { this.send("castVotes"); }
|
||||
},
|
||||
|
||||
castVotes() {
|
||||
if (!this.get("canCastVotes")) { return; }
|
||||
if (!this.currentUser) { return this.send("showLogin"); }
|
||||
|
||||
const self = this;
|
||||
this.get('poll').saveVote(option).then(function() {
|
||||
self.setProperties({ loading: false, showResults: true});
|
||||
|
||||
this.set("loading", true);
|
||||
|
||||
Discourse.ajax("/polls/vote", {
|
||||
type: "PUT",
|
||||
data: {
|
||||
post_id: this.get("post.id"),
|
||||
poll_name: this.get("poll.name"),
|
||||
options: this.get("selectedOptions"),
|
||||
}
|
||||
}).then(function(results) {
|
||||
self.setProperties({ vote: results.vote, showingResults: true });
|
||||
self.set("model", Em.Object.create(results.poll));
|
||||
}).catch(function() {
|
||||
bootbox.alert(I18n.t("poll.error_while_casting_votes"));
|
||||
}).finally(function() {
|
||||
self.set("loading", false);
|
||||
});
|
||||
},
|
||||
|
||||
toggleShowResults() {
|
||||
this.toggleProperty('showResults');
|
||||
toggleResults() {
|
||||
this.toggleProperty("showResults");
|
||||
},
|
||||
|
||||
toggleClosePoll() {
|
||||
const self = this;
|
||||
toggleStatus() {
|
||||
if (!this.get("canToggleStatus")) { return; }
|
||||
|
||||
this.set('loading', true);
|
||||
const self = this,
|
||||
confirm = this.get("isClosed") ? "poll.open.confirm" : "poll.close.confirm";
|
||||
|
||||
return Discourse.ajax('/poll/toggle_close', {
|
||||
type: 'PUT',
|
||||
data: { post_id: this.get('poll.post.id') }
|
||||
}).then(function(result) {
|
||||
self.set('poll.post.topic.title', result.basic_topic.title);
|
||||
self.set('poll.post.topic.fancy_title', result.basic_topic.title);
|
||||
self.set('loading', false);
|
||||
});
|
||||
}
|
||||
bootbox.confirm(
|
||||
I18n.t(confirm),
|
||||
I18n.t("no_value"),
|
||||
I18n.t("yes_value"),
|
||||
function(confirmed) {
|
||||
if (confirmed) {
|
||||
self.set("loading", true);
|
||||
|
||||
Discourse.ajax("/polls/toggle_status", {
|
||||
type: "PUT",
|
||||
data: {
|
||||
post_id: self.get("post.id"),
|
||||
poll_name: self.get("poll.name"),
|
||||
status: self.get("isClosed") ? "open" : "closed",
|
||||
}
|
||||
}).then(function(results) {
|
||||
self.set("model", Em.Object.create(results.poll));
|
||||
}).catch(function() {
|
||||
bootbox.alert(I18n.t("poll.error_while_toggling_status"));
|
||||
}).finally(function() {
|
||||
self.set("loading", false);
|
||||
});
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
},
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
{{{averageRating}}}
|
||||
@@ -0,0 +1,13 @@
|
||||
<tbody>
|
||||
{{#each option in options}}
|
||||
<tr>
|
||||
<td class="option">{{{option.html}}}</td>
|
||||
<td class="percentage">{{option.percentage}}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2" class="bar-back">
|
||||
<div class="bar" {{bind-attr style=option.style}}></div>
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</tbody>
|
||||
@@ -1,37 +1,36 @@
|
||||
<table>
|
||||
{{#each po in poll.options}}
|
||||
<tr {{bind-attr class="po.checked:active"}} {{action "selectOption" po.option}}>
|
||||
<td class="radio">
|
||||
<input type="radio" name="poll" {{bind-attr checked="po.checked" disabled="disableRadio"}}>
|
||||
</td>
|
||||
<td class="option">
|
||||
<div class="option">{{{po.option}}}</div>
|
||||
{{#if showResults}}
|
||||
<div class="result">{{i18n 'poll.voteCount' count=po.votes}}</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
|
||||
<div class='row'>
|
||||
<button {{action "toggleShowResults"}} class="btn btn-small show-results">
|
||||
{{#if showResults}}
|
||||
{{fa-icon "eye-slash"}} {{i18n 'poll.results.hide'}}
|
||||
<div class="poll-container">
|
||||
{{#if showingResults}}
|
||||
{{#if isNumber}}
|
||||
{{poll-results-number poll=poll}}
|
||||
{{else}}
|
||||
{{fa-icon "eye"}} {{i18n 'poll.results.show'}}
|
||||
{{poll-results-standard poll=poll}}
|
||||
{{/if}}
|
||||
</button>
|
||||
|
||||
{{#if showToggleClosePoll}}
|
||||
<button {{action "toggleClosePoll"}} class="btn btn-small toggle-poll">
|
||||
{{#if poll.closed}}
|
||||
{{fa-icon "unlock-alt"}} {{i18n 'poll.open_poll'}}
|
||||
{{else}}
|
||||
{{fa-icon "lock"}} {{i18n 'poll.close_poll'}}
|
||||
{{/if}}
|
||||
</button>
|
||||
{{else}}
|
||||
<ul>
|
||||
{{#each option in poll.options}}
|
||||
{{poll-option option=option color=poll.color background=poll.background toggle="toggleOption"}}
|
||||
{{/each}}
|
||||
</ul>
|
||||
{{/if}}
|
||||
</div>
|
||||
|
||||
{{loading-spinner condition=loading}}
|
||||
<p>{{totalVotesText}}</p>
|
||||
|
||||
{{#if isMultiple}}
|
||||
<p>{{multipleHelpText}}</p>
|
||||
{{d-button class="cast-votes" title="poll.cast-votes.title" label="poll.cast-votes.label" disabled=castVotesDisabled action="castVotes"}}
|
||||
{{/if}}
|
||||
|
||||
{{#if showingResults}}
|
||||
{{d-button class="toggle-results" title="poll.hide-results.title" label="poll.hide-results.label" icon="eye-slash" disabled=hideResultsDisabled action="toggleResults"}}
|
||||
{{else}}
|
||||
{{d-button class="toggle-results" title="poll.show-results.title" label="poll.show-results.label" icon="eye" disabled=showResultsDisabled action="toggleResults"}}
|
||||
{{/if}}
|
||||
|
||||
{{#if canToggleStatus}}
|
||||
{{#if isClosed}}
|
||||
{{d-button class="toggle-status" title="poll.open.title" label="poll.open.label" icon="unlock-alt" action="toggleStatus"}}
|
||||
{{else}}
|
||||
{{d-button class="toggle-status btn-danger" title="poll.close.title" label="poll.close.label" icon="lock" action="toggleStatus"}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
import PostView from "discourse/views/post";
|
||||
|
||||
function createPollView(container, post, poll, vote) {
|
||||
const controller = container.lookup("controller:poll", { singleton: false }),
|
||||
view = container.lookup("view:poll");
|
||||
|
||||
controller.set("vote", vote);
|
||||
|
||||
controller.setProperties({
|
||||
model: Em.Object.create(poll),
|
||||
post: post,
|
||||
});
|
||||
|
||||
view.set("controller", controller);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
export default {
|
||||
name: "extend-for-poll",
|
||||
|
||||
initialize(container) {
|
||||
|
||||
// overwrite polls
|
||||
PostView.reopen({
|
||||
_createPollViews: function($post) {
|
||||
const self = this,
|
||||
post = this.get("post"),
|
||||
polls = post.get("polls"),
|
||||
votes = post.get("polls_votes") || {};
|
||||
|
||||
// don't even bother when there's no poll
|
||||
if (!polls) { return; }
|
||||
|
||||
const pollViews = {};
|
||||
|
||||
// iterate over all polls
|
||||
$(".poll", $post).each(function() {
|
||||
const $div = $("<div>"),
|
||||
$poll = $(this),
|
||||
pollName = $poll.data("poll-name"),
|
||||
pollView = createPollView(container, post, polls[pollName], votes[pollName]);
|
||||
|
||||
$poll.replaceWith($div);
|
||||
pollView.constructor.renderer.replaceIn(pollView, $div[0]);
|
||||
pollViews[pollName] = pollView;
|
||||
});
|
||||
|
||||
this.messageBus.subscribe("/polls/" + this.get("post.id"), results => {
|
||||
pollViews[results.poll.name].get("controller").set("model", Em.Object.create(results.poll));
|
||||
});
|
||||
|
||||
this.set("pollViews", pollViews);
|
||||
}.on("postViewInserted"),
|
||||
|
||||
_cleanUpPollViews: function() {
|
||||
this.messageBus.unsubscribe("/polls/*");
|
||||
|
||||
if (this.get("pollViews")) {
|
||||
_.forEach(this.get("pollViews"), v => v.destroy());
|
||||
}
|
||||
}.on("willClearRender")
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
import Poll from "discourse/plugins/poll/models/poll";
|
||||
import PollView from "discourse/plugins/poll/views/poll";
|
||||
import PollController from "discourse/plugins/poll/controllers/poll";
|
||||
|
||||
import PostView from "discourse/views/post";
|
||||
|
||||
function initializePollView(self) {
|
||||
const post = self.get('post'),
|
||||
pollDetails = post.get('poll_details');
|
||||
|
||||
let poll = Poll.create({ post: post });
|
||||
poll.updateFromJson(pollDetails);
|
||||
|
||||
const pollController = PollController.create({
|
||||
poll: poll,
|
||||
showResults: pollDetails["selected"],
|
||||
postController: self.get('controller')
|
||||
});
|
||||
|
||||
return self.createChildView(PollView, { controller: pollController });
|
||||
}
|
||||
|
||||
export default {
|
||||
name: 'poll',
|
||||
|
||||
initialize: function() {
|
||||
PostView.reopen({
|
||||
createPollUI: function($post) {
|
||||
if (!this.get('post').get('poll_details')) {
|
||||
return;
|
||||
}
|
||||
|
||||
let view = initializePollView(this),
|
||||
pollContainer = $post.find(".poll-ui:first");
|
||||
|
||||
if (pollContainer.length === 0) {
|
||||
pollContainer = $post.find("ul:first");
|
||||
}
|
||||
|
||||
let $div = $('<div>');
|
||||
pollContainer.replaceWith($div);
|
||||
view.constructor.renderer.appendTo(view, $div[0]);
|
||||
this.set('pollView', view);
|
||||
}.on('postViewInserted'),
|
||||
|
||||
clearPollView: function() {
|
||||
if (this.get('pollView')) { this.get('pollView').destroy(); }
|
||||
}.on('willClearRender')
|
||||
});
|
||||
}
|
||||
};
|
||||
16
plugins/poll/assets/javascripts/lib/decimal-adjust.js.es6
Normal file
16
plugins/poll/assets/javascripts/lib/decimal-adjust.js.es6
Normal file
@@ -0,0 +1,16 @@
|
||||
// from: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Math/floor
|
||||
|
||||
export default function(type, value, exp) {
|
||||
// If the exp is undefined or zero...
|
||||
if (typeof exp === 'undefined' || +exp === 0) { return Math[type](value); }
|
||||
value = +value;
|
||||
exp = +exp;
|
||||
// If the value is not a number or the exp is not an integer...
|
||||
if (isNaN(value) || !(typeof exp === 'number' && exp % 1 === 0)) { return NaN; }
|
||||
// Shift
|
||||
value = value.toString().split('e');
|
||||
value = Math[type](+(value[0] + 'e' + (value[1] ? (+value[1] - exp) : -exp)));
|
||||
// Shift back
|
||||
value = value.toString().split('e');
|
||||
return +(value[0] + 'e' + (value[1] ? (+value[1] + exp) : exp));
|
||||
}
|
||||
5
plugins/poll/assets/javascripts/lib/round.js.es6
Normal file
5
plugins/poll/assets/javascripts/lib/round.js.es6
Normal file
@@ -0,0 +1,5 @@
|
||||
import decimalAdjust from "discourse/plugins/poll/lib/decimal-adjust";
|
||||
|
||||
export default function(value, exp) {
|
||||
return decimalAdjust("round", value, exp);
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
export default Discourse.Model.extend({
|
||||
post: null,
|
||||
options: [],
|
||||
closed: false,
|
||||
|
||||
postObserver: function() {
|
||||
this.updateFromJson(this.get('post.poll_details'));
|
||||
}.observes('post.poll_details'),
|
||||
|
||||
fetchNewPostDetails: Discourse.debounce(function() {
|
||||
this.get('post.topic.postStream').triggerChangedPost(this.get('post.id'), this.get('post.topic.updated_at'));
|
||||
}, 250).observes('post.topic.title'),
|
||||
|
||||
updateFromJson(json) {
|
||||
const selectedOption = json["selected"];
|
||||
let options = [];
|
||||
|
||||
Object.keys(json["options"]).forEach(function(option) {
|
||||
options.push(Ember.Object.create({
|
||||
option: option,
|
||||
votes: json["options"][option],
|
||||
checked: option === selectedOption
|
||||
}));
|
||||
});
|
||||
|
||||
this.setProperties({ options: options, closed: json.closed });
|
||||
},
|
||||
|
||||
saveVote(option) {
|
||||
this.get('options').forEach(function(opt) {
|
||||
opt.set('checked', opt.get('option') === option);
|
||||
});
|
||||
|
||||
const self = this;
|
||||
return Discourse.ajax("/poll", {
|
||||
type: "PUT",
|
||||
data: { post_id: this.get('post.id'), option: option }
|
||||
}).then(function(newJSON) {
|
||||
self.updateFromJson(newJSON);
|
||||
});
|
||||
}
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
Discourse.Dialect.inlineBetween({
|
||||
start: '[poll]',
|
||||
stop: '[/poll]',
|
||||
rawContents: true,
|
||||
emitter: function(contents) {
|
||||
var list = Discourse.Dialect.cook(contents, {});
|
||||
return ['div', { class: 'poll-ui' }, list];
|
||||
}
|
||||
});
|
||||
149
plugins/poll/assets/javascripts/poll_dialect.js
Normal file
149
plugins/poll/assets/javascripts/poll_dialect.js
Normal file
@@ -0,0 +1,149 @@
|
||||
(function() {
|
||||
|
||||
const DATA_PREFIX = "data-poll-";
|
||||
const DEFAULT_POLL_NAME = "poll";
|
||||
|
||||
const WHITELISTED_ATTRIBUTES = ["type", "name", "min", "max", "step", "order", "color", "background", "status"];
|
||||
const WHITELISTED_STYLES = ["color", "background"];
|
||||
|
||||
const ATTRIBUTES_REGEX = new RegExp("(" + WHITELISTED_ATTRIBUTES.join("|") + ")=[^\\s\\]]+", "g");
|
||||
|
||||
Discourse.Dialect.replaceBlock({
|
||||
start: /\[poll([^\]]*)\]([\s\S]*)/igm,
|
||||
stop: /\[\/poll\]/igm,
|
||||
|
||||
emitter: function(blockContents, matches, options) {
|
||||
// post-process inside block contents
|
||||
var contents = [];
|
||||
|
||||
if (blockContents.length) {
|
||||
var self = this, b;
|
||||
while ((b = blockContents.shift()) !== undefined) {
|
||||
this.processBlock(b, blockContents).forEach(function (bc) {
|
||||
if (typeof bc === "string" || bc instanceof String) {
|
||||
var processed = self.processInline(String(bc));
|
||||
if (processed.length) {
|
||||
contents.push(["p"].concat(processed));
|
||||
}
|
||||
} else {
|
||||
contents.push(bc);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// default poll attributes
|
||||
var attributes = { "class": "poll" };
|
||||
attributes[DATA_PREFIX + "status"] = "open";
|
||||
attributes[DATA_PREFIX + "name"] = DEFAULT_POLL_NAME;
|
||||
|
||||
// extract poll attributes
|
||||
(matches[1].match(ATTRIBUTES_REGEX) || []).forEach(function(m) {
|
||||
var attr = m.split("=");
|
||||
attributes[DATA_PREFIX + attr[0]] = attr[1];
|
||||
});
|
||||
|
||||
// we might need these values later...
|
||||
var min = parseInt(attributes[DATA_PREFIX + "min"], 10),
|
||||
max = parseInt(attributes[DATA_PREFIX + "max"], 10),
|
||||
step = parseInt(attributes[DATA_PREFIX + "step"], 10);
|
||||
|
||||
// generate the options when the type is "number"
|
||||
if (attributes[DATA_PREFIX + "type"] === "number") {
|
||||
// default values
|
||||
if (isNaN(min)) { min = 1; }
|
||||
if (isNaN(max)) { max = 10; }
|
||||
if (isNaN(step)) { step = 1; }
|
||||
// dynamically generate options
|
||||
contents.push(["bulletlist"]);
|
||||
for (var o = min; o <= max; o += step) {
|
||||
contents[0].push(["listitem", String(o)]);
|
||||
}
|
||||
}
|
||||
|
||||
// make sure the first child is a list with at least 1 option
|
||||
if (contents.length === 0 || contents[0].length <= 1 || (contents[0][0] !== "numberlist" && contents[0][0] !== "bulletlist")) {
|
||||
return ["div"].concat(contents);
|
||||
}
|
||||
|
||||
// TODO: remove non whitelisted content
|
||||
|
||||
// generate <li> styles (if any)
|
||||
var styles = [];
|
||||
WHITELISTED_STYLES.forEach(function(style) {
|
||||
if (attributes[DATA_PREFIX + style]) {
|
||||
styles.push(style + ":" + attributes[DATA_PREFIX + style]);
|
||||
}
|
||||
});
|
||||
|
||||
var style = styles.join(";");
|
||||
|
||||
// add option id (hash) + style
|
||||
for (var o = 1; o < contents[0].length; o++) {
|
||||
// break as soon as the list is done
|
||||
if (contents[0][o][0] !== "listitem") { break; }
|
||||
|
||||
var attr = {};
|
||||
// apply styles if any
|
||||
if (style.length > 0) { attr["style"] = style; }
|
||||
// compute md5 hash of the content of the option
|
||||
attr[DATA_PREFIX + "option-id"] = md5(JSON.stringify(contents[0][o].slice(1)));
|
||||
// store options attributes
|
||||
contents[0][o].splice(1, 0, attr);
|
||||
}
|
||||
|
||||
// that's our poll!
|
||||
var result = ["div", attributes].concat(contents);
|
||||
|
||||
// add a small paragraph displaying the total number of votes
|
||||
result.push(["p", I18n.t("poll.total_votes", { count: 0 })]);
|
||||
|
||||
// add some information when type is "multiple"
|
||||
if (attributes[DATA_PREFIX + "type"] === "multiple") {
|
||||
var optionCount = contents[0].length - 1;
|
||||
|
||||
// default values
|
||||
if (isNaN(min) || min < 1) { min = 1; }
|
||||
if (isNaN(max) || max > optionCount) { max = optionCount; }
|
||||
|
||||
// add some help text
|
||||
var 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 < optionCount) {
|
||||
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 <= optionCount) {
|
||||
help = I18n.t("poll.multiple.help.up_to_max_options", { count: max });
|
||||
}
|
||||
}
|
||||
|
||||
if (help) { result.push(["p", help]); }
|
||||
|
||||
// add "cast-votes" button
|
||||
result.push(["a", { "class": "button cast-votes", "title": I18n.t("poll.cast-votes.title") }, I18n.t("poll.cast-votes.label")]);
|
||||
}
|
||||
|
||||
// add "toggle-results" button
|
||||
result.push(["a", { "class": "button toggle-results", "title": I18n.t("poll.show-results.title") }, I18n.t("poll.show-results.label")]);
|
||||
|
||||
return result;
|
||||
}
|
||||
});
|
||||
|
||||
Discourse.Markdown.whiteListTag("div", "class", "poll");
|
||||
Discourse.Markdown.whiteListTag("div", "data-*");
|
||||
|
||||
Discourse.Markdown.whiteListTag("a", "class", /^button (cast-votes|toggle-results)/);
|
||||
|
||||
Discourse.Markdown.whiteListTag("li", "data-*");
|
||||
Discourse.Markdown.whiteListTag("li", "style");
|
||||
|
||||
})();
|
||||
@@ -1,4 +1,16 @@
|
||||
export default Ember.View.extend({
|
||||
export default Em.View.extend({
|
||||
templateName: "poll",
|
||||
classNames: ['poll-ui'],
|
||||
classNames: ["poll"],
|
||||
attributeBindings: ["data-poll-type", "data-poll-name", "data-poll-status"],
|
||||
|
||||
poll: Em.computed.alias("controller.poll"),
|
||||
|
||||
"data-poll-type": Em.computed.alias("poll.type"),
|
||||
"data-poll-name": Em.computed.alias("poll.name"),
|
||||
"data-poll-status": Em.computed.alias("poll.status"),
|
||||
|
||||
_fixPollContainerHeight: function() {
|
||||
const pollContainer = this.$(".poll-container");
|
||||
pollContainer.height(pollContainer.height());
|
||||
}.on("didInsertElement")
|
||||
});
|
||||
|
||||
105
plugins/poll/assets/stylesheets/poll.scss
Normal file
105
plugins/poll/assets/stylesheets/poll.scss
Normal file
@@ -0,0 +1,105 @@
|
||||
div.poll {
|
||||
|
||||
ul, ol {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
display: inline-block;
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
li, .option {
|
||||
cursor: pointer;
|
||||
font-size: 1.125em;
|
||||
line-height: 2;
|
||||
}
|
||||
|
||||
li[data-poll-option-id] {
|
||||
color: $secondary;
|
||||
background: $primary;
|
||||
padding: 0 .8em;
|
||||
margin-bottom: .7em;
|
||||
border-radius: .25rem;
|
||||
box-shadow: inset 0 -.2em 0 0 rgba(0,0,0,.2),
|
||||
inset 0 0 0 100px rgba(0,0,0,0),
|
||||
0 .2em 0 0 rgba(0,0,0,.2);
|
||||
|
||||
&:hover {
|
||||
box-shadow: inset 0 -.2em 0 0 rgba(0,0,0,.25),
|
||||
inset 0 0 0 100px rgba(0,0,0,.1),
|
||||
0 .2em 0 0 rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
&:active {
|
||||
-webkit-transform: translate(0,2px);
|
||||
transform: translate(0,2px);
|
||||
box-shadow: inset 0 -.1em 0 0 rgba(0,0,0,.25),
|
||||
inset 0 0 0 100px rgba(0,0,0,.1),
|
||||
0 .1em 0 0 rgba(0,0,0,.2);
|
||||
}
|
||||
|
||||
&[data-poll-selected="selected"] {
|
||||
background: green !important;
|
||||
}
|
||||
}
|
||||
|
||||
.button {
|
||||
display: inline-block;
|
||||
padding: 6px 12px;
|
||||
margin-right: 5px;
|
||||
text-align: center;
|
||||
cursor: pointer;
|
||||
color: $primary;
|
||||
background: dark-light-diff($primary, $secondary, 90%, -65%);
|
||||
|
||||
&:hover {
|
||||
background: dark-light-diff($primary, $secondary, 65%, -75%);
|
||||
color: #fff;
|
||||
}
|
||||
}
|
||||
|
||||
.poll-container {
|
||||
margin: 0;
|
||||
span {
|
||||
font-size: 1.125em;
|
||||
line-height: 2
|
||||
}
|
||||
}
|
||||
|
||||
.results {
|
||||
|
||||
.option {
|
||||
max-width: 90%;
|
||||
padding-right: 1.6em;
|
||||
}
|
||||
|
||||
.percentage {
|
||||
width: 10%;
|
||||
font-size: 1.7em;
|
||||
text-align: right;
|
||||
vertical-align: middle;
|
||||
color: #9E9E9E;
|
||||
}
|
||||
|
||||
.bar-back {
|
||||
background: rgb(219,219,219);
|
||||
}
|
||||
|
||||
.bar {
|
||||
height: 10px;
|
||||
background: $primary;
|
||||
transition: all 0.25s;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
&[data-poll-type="number"] {
|
||||
|
||||
li {
|
||||
display: inline-block;
|
||||
margin-right: .7em;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
ar:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
zero: "صوت 1"
|
||||
one: "صوت 1"
|
||||
two: "صوت 1"
|
||||
few: "صوت 1"
|
||||
many: "%{احسب} الأصوات"
|
||||
other: "%{احسب} الأصوات"
|
||||
results:
|
||||
show: إظهار النتائج
|
||||
hide: إخفاء النتائج
|
||||
close_poll: "إغلاق التصويت"
|
||||
open_poll: "فتح التصويت"
|
||||
@@ -1,11 +0,0 @@
|
||||
ca:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 vot"
|
||||
other: "%{count} vots"
|
||||
results:
|
||||
show: Mostra resultats
|
||||
hide: Amaga resultats
|
||||
close_poll: "Tanca enquesta"
|
||||
open_poll: "Obre enquesta"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
de:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 Stimme"
|
||||
other: "%{count} Stimmen"
|
||||
results:
|
||||
show: Ergebnisse anzeigen
|
||||
hide: Ergebnisse ausblenden
|
||||
close_poll: "Umfrage beenden"
|
||||
open_poll: "Umfrage starten"
|
||||
@@ -1,30 +1,41 @@
|
||||
# encoding: utf-8
|
||||
# This file contains content for the client portion of Discourse, sent out
|
||||
# to the Javascript app.
|
||||
#
|
||||
# To work with us on translations, see:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
#
|
||||
# This is a "source" file, which is used by Transifex to get translations for other languages.
|
||||
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
|
||||
#
|
||||
# tx push -s
|
||||
#
|
||||
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
|
||||
#
|
||||
# To validate this YAML file after you change it, please paste it into
|
||||
# http://yamllint.com/
|
||||
|
||||
en:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 vote"
|
||||
other: "%{count} votes"
|
||||
total_votes:
|
||||
zero: "No votes yet. Want to be the first?"
|
||||
one: "There's only 1 vote."
|
||||
other: "There are %{count} total votes."
|
||||
|
||||
results:
|
||||
show: Show Results
|
||||
hide: Hide Results
|
||||
average_rating: "Average rating: <strong>%{average}</strong>."
|
||||
|
||||
close_poll: "Close Poll"
|
||||
open_poll: "Open Poll"
|
||||
multiple:
|
||||
help:
|
||||
at_least_min_options: "You may choose at least %{count} options."
|
||||
up_to_max_options: "You may choose up to %{count} options."
|
||||
x_options: "You may choose %{count} options."
|
||||
between_min_and_max_options: "You may choose between %{min} and %{max} options."
|
||||
|
||||
cast-votes:
|
||||
title: "Cast your votes"
|
||||
label: "Vote now!"
|
||||
|
||||
show-results:
|
||||
title: "Display the poll results"
|
||||
label: "Show results"
|
||||
|
||||
hide-results:
|
||||
title: "Back to your votes"
|
||||
label: "Hide results"
|
||||
|
||||
open:
|
||||
title: "Open the poll"
|
||||
label: "Open"
|
||||
confirm: "Are you sure you want to open this poll?"
|
||||
|
||||
close:
|
||||
title: "Close the poll"
|
||||
label: "Close"
|
||||
confirm: "Are you sure you want to close this poll?"
|
||||
|
||||
error_while_toggling_status: "There was an error while toggling the status of this poll."
|
||||
error_while_casting_votes: "There was an error while casting your votes."
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
es:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 voto"
|
||||
other: "%{count} votos"
|
||||
results:
|
||||
show: Mostrar resultados
|
||||
hide: Ocultar resultados
|
||||
close_poll: "Cerrar encuesta"
|
||||
open_poll: "Abrir encuesta"
|
||||
@@ -1,17 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
fa_IR:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
other: "%{count} آرا"
|
||||
results:
|
||||
show: نمایش نتایج
|
||||
hide: پنهان کرد نتایج
|
||||
close_poll: "بستن نظرسنجی"
|
||||
open_poll: "باز کردن نظرسنجی"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
fi:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 ääni"
|
||||
other: "%{count} ääntä"
|
||||
results:
|
||||
show: Näytä tulokset
|
||||
hide: Piilota tulokset
|
||||
close_poll: "Sulje kysely"
|
||||
open_poll: "Avaa kysely"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
fr:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 vote"
|
||||
other: "%{count} votes"
|
||||
results:
|
||||
show: Voir les résultats
|
||||
hide: Cacher les résultats
|
||||
close_poll: "Fermer le sondage"
|
||||
open_poll: "Réouvrir le sondage"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
he:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "הצבעה אחת"
|
||||
other: "%{count} הצבעות"
|
||||
results:
|
||||
show: הצגת תוצאות
|
||||
hide: הסתרת תוצאות
|
||||
close_poll: "סגירת הצבעה"
|
||||
open_poll: "פתיחת הצבעה"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
it:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 voto"
|
||||
other: "%{count} voti"
|
||||
results:
|
||||
show: Mostra Risultati
|
||||
hide: Nascondi Risultati
|
||||
close_poll: "Chiudi Sondaggio"
|
||||
open_poll: "Apri Sondaggio"
|
||||
@@ -1,17 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
ko:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
other: "%{count} 표"
|
||||
results:
|
||||
show: 결과 보기
|
||||
hide: 결과 숨기기
|
||||
close_poll: "투표 끝내기"
|
||||
open_poll: "투표 시작하기"
|
||||
@@ -1,19 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
pl_PL:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 głos"
|
||||
few: "%{count} głosy"
|
||||
other: "%{count} głosów"
|
||||
results:
|
||||
show: Pokaż wyniki
|
||||
hide: Ukryj wyniki
|
||||
close_poll: "Zamknij ankietę"
|
||||
open_poll: "Otwórz ankietę"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
pt:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 voto"
|
||||
other: "%{count} votos"
|
||||
results:
|
||||
show: Mostrar resultados
|
||||
hide: Esconder resultados
|
||||
close_poll: "Encerrar votação"
|
||||
open_poll: "Abrir votação"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
pt_BR:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 voto"
|
||||
other: "%{count} votos"
|
||||
results:
|
||||
show: Mostrar Resultados
|
||||
hide: Esconder Resultados
|
||||
close_poll: "Fechar Enquete "
|
||||
open_poll: "Enquete aberta"
|
||||
@@ -1,19 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
ru:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "проголосовал 1"
|
||||
few: "проголосовало %{count}"
|
||||
other: "проголосовало %{count}"
|
||||
results:
|
||||
show: Показать результаты
|
||||
hide: Скрыть результаты
|
||||
close_poll: "Завершить опрос"
|
||||
open_poll: "Запустить опрос снова"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
sq:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "1 votë"
|
||||
other: "%{count} vota"
|
||||
results:
|
||||
show: Shfaq Rezultatet
|
||||
hide: Fsheh Rezultate
|
||||
close_poll: "Mbyll Sondazhin"
|
||||
open_poll: "Hap Sondazhin"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
te:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
one: "ఒక ఓటు"
|
||||
other: "%{count} ఓట్లు"
|
||||
results:
|
||||
show: ఫలితాలు చూపించు
|
||||
hide: ఫలితాలు దాయు
|
||||
close_poll: "ఓటు ముగించు"
|
||||
open_poll: "ఓటు తెరువు"
|
||||
@@ -1,17 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
tr_TR:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
other: "%{count} oy"
|
||||
results:
|
||||
show: Sonuçları göster
|
||||
hide: Sonuçları gizle
|
||||
close_poll: "Anketi Bitir"
|
||||
open_poll: "Anket Başlat"
|
||||
@@ -1,17 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
zh_CN:
|
||||
js:
|
||||
poll:
|
||||
voteCount:
|
||||
other: "%{count} 次投票"
|
||||
results:
|
||||
show: 显示结果
|
||||
hide: 隐藏结果
|
||||
close_poll: "关闭投票"
|
||||
open_poll: "开始投票"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
ar:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "خيارات التصويت"
|
||||
poll:
|
||||
must_contain_poll_options: "يجب أن يحتوي على قائمة خيارات التصويت"
|
||||
cannot_have_modified_options: "التعديل غير ممكن بعد مضي 5 دقائق. اتصل بالمسؤول إذا كنت بحاجة لتغييرها."
|
||||
cannot_add_or_remove_options: "تستطيع تعديله فقط ولا يمكنك إضافته أو حذفه. إذا كنت بحاجة لإضافة أو حذف خيارات يجب أن تُقفل هذا العنوان وتنشئ عنوان جديد."
|
||||
prefix: "تصويت"
|
||||
closed_prefix: "هذا التصويت مغلق"
|
||||
@@ -1,11 +0,0 @@
|
||||
ca:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Opcions d'enquesta"
|
||||
poll:
|
||||
must_contain_poll_options: "cal que contingui una llista d'opcions"
|
||||
cannot_have_modified_options: "no es pot modificar quan hagin passat els primers cinc minuts. Contacta un moderador si necessites fer-hi canvis."
|
||||
cannot_add_or_remove_options: "només es pot editar, no afegir o treure. Si necessites afegir o treure opcions, hauries de tancar aquest tema i crear-ne un de nou."
|
||||
prefix: "Enquesta"
|
||||
closed_prefix: "Enquesta tancada"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
de:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Umfrageoptionen"
|
||||
poll:
|
||||
must_contain_poll_options: "muss eine Liste mit Umfrageoptionen enthalten"
|
||||
cannot_have_modified_options: "können nach den ersten 5 Minuten nicht mehr geändert werden. Kontaktiere einen Moderator, wenn du sie ändern möchtest."
|
||||
cannot_add_or_remove_options: "können nur bearbeitet, jedoch nicht hinzugefügt oder entfernt werden. Wenn du Optionen hinzufügen oder entfernen möchtest, solltest du dieses Thema sperren und ein neues erstellen."
|
||||
prefix: "Umfrage"
|
||||
closed_prefix: "Beendete Umfrage"
|
||||
@@ -1,28 +1,25 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# This file contains content for the server portion of Discourse used by Ruby
|
||||
#
|
||||
# To work with us on translations, see:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
#
|
||||
# This is a "source" file, which is used by Transifex to get translations for other languages.
|
||||
# After this file is changed, it needs to be pushed by a maintainer to Transifex:
|
||||
#
|
||||
# tx push -s
|
||||
#
|
||||
# Read more here: https://meta.discourse.org/t/contribute-a-translation-to-discourse/14882
|
||||
#
|
||||
# To validate this YAML file after you change it, please paste it into
|
||||
# http://yamllint.com/
|
||||
|
||||
en:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Poll options"
|
||||
site_settings:
|
||||
poll_enabled: "Allow users to create polls?"
|
||||
|
||||
poll:
|
||||
must_contain_poll_options: "must contain a list of poll options"
|
||||
cannot_have_modified_options: "cannot be modified after the first five minutes. Contact a moderator if you need to change them."
|
||||
cannot_add_or_remove_options: "can only be edited, not added or removed. If you need to add or remove options you should lock this topic and create a new one."
|
||||
prefix: "Poll"
|
||||
closed_prefix: "Closed Poll"
|
||||
multiple_polls_without_name: "There are multiple polls without a name. Use the '<code>name</code>' attribute to uniquely identify your polls."
|
||||
multiple_polls_with_same_name: "There are multiple polls with the same name: <strong>%{name}</strong>. Use the '<code>name</code>' attribute to uniquely identify your polls."
|
||||
|
||||
default_poll_must_have_at_least_2_options: "Poll must have at least 2 options."
|
||||
named_poll_must_have_at_least_2_options: "Poll named <strong>%{name}</strong> must have at least 2 options."
|
||||
|
||||
default_poll_must_have_different_options: "Poll must have different options."
|
||||
named_poll_must_have_different_options: "Poll name <strong>%{name}</strong> must have different options."
|
||||
|
||||
cannot_change_polls_after_5_minutes: "Polls cannot be changed after the first 5 minutes. Contact a moderator if you need to change them."
|
||||
staff_cannot_add_or_remove_options_after_5_minutes: "Poll options can only be edited after the first 5 minutes. If you need to add or remove options, you should close this topic and create a new one."
|
||||
|
||||
no_polls_associated_with_this_post: "No polls are associated with this post."
|
||||
no_poll_with_this_name: "No poll named <strong>%{name}</strong> associated with this post."
|
||||
|
||||
topic_must_be_open_to_vote: "The topic must be open to vote."
|
||||
poll_must_be_open_to_vote: "Poll must be open to vote."
|
||||
|
||||
topic_must_be_open_to_toggle_status: "The topic must be open to toggle status."
|
||||
only_staff_or_op_can_toggle_status: "Only a staff member or the original poster can toggle a poll status."
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
es:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Opciones de la encuesta"
|
||||
poll:
|
||||
must_contain_poll_options: "debe contener una lista con las opciones de la encuesta"
|
||||
cannot_have_modified_options: "pasados 5 minutos, no se pueden modificar las opciones de la encuesta. Contacta un moderador si necesitas cambiarlas"
|
||||
cannot_add_or_remove_options: "solo se pueden modificar, no añadir ni eliminar. Si necesitas añadir o eliminar opciones deberías cerrar este tema y crear una encuesta nueva."
|
||||
prefix: "Encuesta"
|
||||
closed_prefix: "Encuesta cerrada"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
fa_IR:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "گزینههای نظرسنجی"
|
||||
poll:
|
||||
must_contain_poll_options: "باید فهرستی شامل گزینههای نظرسنجی باشد"
|
||||
cannot_have_modified_options: "پس از گذشت ۵ دقیقه دیگر نمیتوان ویرایش کرد. اگر تغییری در آنها نیاز است با یکی از ناظمان تماس بگیرید."
|
||||
cannot_add_or_remove_options: "تنها میتواند ویرایش شود، نه افزودنی و نه پاک کردنی. اگر به گزینههای اضافه و پاک کردن نیاز دارید، باید این جستار را قفل کنید و یکی دیگر بسازید."
|
||||
prefix: "نظرسنجی"
|
||||
closed_prefix: "اتمام نظرسنجی"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
fi:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Kyselyn vaihtoehtoja"
|
||||
poll:
|
||||
must_contain_poll_options: "täytyy sisältää lista vastausvaihtoehdoista"
|
||||
cannot_have_modified_options: "ei voi muokata kun viisi minuuttia on kulunut kyselyn luomisesta. Ota yhteyttä valvojaan jos sinun tarvitsee muokata vaihtoehtoja."
|
||||
cannot_add_or_remove_options: "voi vain muokata, ei lisätä tai poistaa. Jos sinun tarvitsee lisätä tai poistaa vaihtoehtoja, sinun tulee lukita tämä ketju ja luoda uusi."
|
||||
prefix: "Kysely"
|
||||
closed_prefix: "Suljettu kysely"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
fr:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Les options du sondage"
|
||||
poll:
|
||||
must_contain_poll_options: "doit contenir une liste d'options pour le sondage"
|
||||
cannot_have_modified_options: "ne peuvent pas être modifiés après 5 minutes. Merci de contacter un moderateur, si vous souhaitez les modifier"
|
||||
cannot_add_or_remove_options: "peuvent seulement être modifiés. Si vous souhaitez en supprimer ou en ajouter, veuillez créer un nouveau sujet."
|
||||
prefix: "Sondage "
|
||||
closed_prefix: "Sondage fermé "
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
he:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "אפשרויות הצבעה"
|
||||
poll:
|
||||
must_contain_poll_options: "חובה להכיל רשימה של אפשרויות הצבעה"
|
||||
cannot_have_modified_options: "לא ניתן לשנות את האפשרויות לאחר 5 הדקות הראשונות. יש לפנות למנהל כדי לבצע שינויים אלו."
|
||||
cannot_add_or_remove_options: "ניתן רק לערוך, לא להוסיף או להסיר אפשרויות. כדי להוסיף או להסיר אפשרויות יש לנעול את נושא זה ולפתוח אחד חדש."
|
||||
prefix: "הצבעה"
|
||||
closed_prefix: "הצבעה סגורה"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
it:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Opzioni sondaggio"
|
||||
poll:
|
||||
must_contain_poll_options: "deve contenere una lista di opzioni per il sondaggio"
|
||||
cannot_have_modified_options: "non possono essere modificate dopo i primi cinque minuti. Contatta un moderatore se devi cambiarle."
|
||||
cannot_add_or_remove_options: "possono essere solo modificate, ma non aggiunte o rimosse. Se devi aggiungere o rimuovere opzioni, devi prima bloccare questo argomento e crearne uno nuovo."
|
||||
prefix: "Sondaggio"
|
||||
closed_prefix: "Sondaggio Chiuso"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
ko:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "투표 옵션"
|
||||
poll:
|
||||
must_contain_poll_options: "투표 옵션 목록 포함 필수"
|
||||
cannot_have_modified_options: "5분 뒤에는 수정할 수 없습니다. 바꾸고 싶다면 관리자에게 문의하세요."
|
||||
cannot_add_or_remove_options: "수정만 가능하고 추가나 삭제가 불가능 합니다. 선택사항을 추가하거나 삭제하고 싶다면 이 토픽을 잠그고 다른 토픽을 생성해야합니다."
|
||||
prefix: "투표"
|
||||
closed_prefix: "투표 닫기"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
pl_PL:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Opcje ankiety"
|
||||
poll:
|
||||
must_contain_poll_options: "musi zawierać listę możliwych wyborów ankiety"
|
||||
cannot_have_modified_options: "nie mogą być zmienione po pierwszych pięciu minutach. Skontaktuj się z moderatorem, jeżeli musisz je zmienić."
|
||||
cannot_add_or_remove_options: "mogą tylko być edytowane, nie dodawane ani usuwane. Jeśli musisz dodać lub usunąć opcje, powinieneś zamknąć ten temat i utworzyć nowy."
|
||||
prefix: "Ankieta"
|
||||
closed_prefix: "Zamknięta ankieta"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
pt:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Opções da votação"
|
||||
poll:
|
||||
must_contain_poll_options: "tem que conter uma lista de opções de votação"
|
||||
cannot_have_modified_options: "não podem ser modificadas depois dos primeiros cinco minutos. Contacte um moderador se precisar de alterá-las."
|
||||
cannot_add_or_remove_options: "podem apenas ser editadas, não podendo ser adicionadas ou removidas. Se precisar de adicionar ou remover opções, deverá bloquear este tópico e criar um novo."
|
||||
prefix: "Votação"
|
||||
closed_prefix: "Votação encerrada"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
pt_BR:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Opções de votação "
|
||||
poll:
|
||||
must_contain_poll_options: "deve conter uma lista de opções de votação"
|
||||
cannot_have_modified_options: "não pode ser modificado após os primeiros cinco minutos. Contate o moderador se necessitar fazer alguma mudança."
|
||||
cannot_add_or_remove_options: "Só pode ser editado, mas não adicionar nem remover. Se precisar das opções para adicionar ou remover, você deve bloquear este tópico e criar um novo."
|
||||
prefix: "Votação"
|
||||
closed_prefix: "Votação encerrada"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
ru:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Варианты ответов"
|
||||
poll:
|
||||
must_contain_poll_options: "должен содержать варианты ответов (список)"
|
||||
cannot_have_modified_options: "нельзя изменять после первых пяти минут. Если все же нужно их отредактировать, свяжитесь с модератором."
|
||||
cannot_add_or_remove_options: "можно редактировать, но не добавлять или удалять. Если нужно добавить или удалить, закройте эту тему и создайте новую."
|
||||
prefix: "Опрос"
|
||||
closed_prefix: "Завершившийся опрос"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
sq:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Opsionet e sondazhit"
|
||||
poll:
|
||||
must_contain_poll_options: "duhet të përmbajë një listë me pyetje"
|
||||
cannot_have_modified_options: "nuk mund të ndryshohet pasi kanë kaluar pesë minuta. Kontakto një moderator nëse nevojiten ndryshime."
|
||||
cannot_add_or_remove_options: "nuk mund të redaktohet, shtosh apo fshini pyetje. Nëse dëshironi të shtoni apo fshini pyetje ju duhet ta mbyllni këtë temë dhe të krijoni një të re."
|
||||
prefix: "Sondazh"
|
||||
closed_prefix: "Sondazh i Mbyllur"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
te:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "ఓటు ఐచ్చికాలు"
|
||||
poll:
|
||||
must_contain_poll_options: "తప్పనిసరి ఓటు ఐచ్చికాల జాబితా కలిగి ఉండాలి"
|
||||
cannot_have_modified_options: "మొదటి ఐదు నిమిషాల తర్వాత మార్చైత కాదు. వీటిని మార్చాలంటే ఒక నిర్వాహకుడిని సంప్రదించండి. "
|
||||
cannot_add_or_remove_options: "కేవలం సవరించవచ్చు, కలపైత కాదు, తొలగించైత కాదు. మీరు కలపడం లేదా తొలగించడం చేయాలంటే ఈ విషయానికి తాళం వేసి మరో కొత్త విషయం సృష్టించాలి"
|
||||
prefix: "ఓటు"
|
||||
closed_prefix: "మూసేసిన ఓటు"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
tr_TR:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "Anket seçenekleri"
|
||||
poll:
|
||||
must_contain_poll_options: "anket seçenekleri listesini içermeli"
|
||||
cannot_have_modified_options: "ilk beş dakikadan sonra değişiklik yapılamaz. Değişiklik yapmanız gerekiyorsa, bir moderatör ile iletişime geçin."
|
||||
cannot_add_or_remove_options: "sadece düzenlenebilir, ekleme veya çıkarma yapılamaz. Seçenek ekleme veya çıkarmanız gerekiyorsa, bu konuyu kitlemeli ve yeni bir konu oluşturmalısınız."
|
||||
prefix: "Anket"
|
||||
closed_prefix: "Bitmiş Anket"
|
||||
@@ -1,18 +0,0 @@
|
||||
# encoding: utf-8
|
||||
#
|
||||
# Never edit this file. It will be overwritten when translations are pulled from Transifex.
|
||||
#
|
||||
# To work with us on translations, join this project:
|
||||
# https://www.transifex.com/projects/p/discourse-org/
|
||||
|
||||
zh_CN:
|
||||
activerecord:
|
||||
attributes:
|
||||
post:
|
||||
poll_options: "投票选项"
|
||||
poll:
|
||||
must_contain_poll_options: "必须包含投票选项"
|
||||
cannot_have_modified_options: "在开始的五分钟后不能修改。如果需要修改他们,请联系一位版主。"
|
||||
cannot_add_or_remove_options: "只能被编辑,不能添加或者删除。如果您需要添加或者删除选项,你需要锁定这个投票并创建新的投票。"
|
||||
prefix: "投票"
|
||||
closed_prefix: "已关闭的投票:"
|
||||
3
plugins/poll/config/settings.yml
Normal file
3
plugins/poll/config/settings.yml
Normal file
@@ -0,0 +1,3 @@
|
||||
plugins:
|
||||
poll_enabled:
|
||||
default: true
|
||||
@@ -1,205 +1,267 @@
|
||||
# name: poll
|
||||
# about: adds poll support to Discourse
|
||||
# version: 0.2
|
||||
# authors: Vikhyat Korrapati
|
||||
# about: Official poll plugin for Discourse
|
||||
# version: 0.9
|
||||
# authors: Vikhyat Korrapati (vikhyat), Régis Hanol (zogstrip)
|
||||
# url: https://github.com/discourse/discourse/tree/master/plugins/poll
|
||||
|
||||
load File.expand_path("../poll.rb", __FILE__)
|
||||
register_asset "stylesheets/poll.scss"
|
||||
register_asset "javascripts/poll_dialect.js", :server_side
|
||||
|
||||
# Without this line we can't lookup the constant inside the after_initialize blocks,
|
||||
# because all of this is instance_eval'd inside an instance of Plugin::Instance.
|
||||
PollPlugin = PollPlugin
|
||||
PLUGIN_NAME ||= "discourse_poll".freeze
|
||||
|
||||
POLLS_CUSTOM_FIELD ||= "polls".freeze
|
||||
VOTES_CUSTOM_FIELD ||= "polls-votes".freeze
|
||||
|
||||
after_initialize do
|
||||
# Rails Engine for accepting votes.
|
||||
module PollPlugin
|
||||
|
||||
module ::DiscoursePoll
|
||||
class Engine < ::Rails::Engine
|
||||
engine_name "poll_plugin"
|
||||
isolate_namespace PollPlugin
|
||||
end
|
||||
|
||||
class PollController < ActionController::Base
|
||||
include CurrentUser
|
||||
|
||||
def vote
|
||||
if current_user.nil?
|
||||
render status: :forbidden, json: false
|
||||
return
|
||||
end
|
||||
|
||||
if params[:post_id].nil? or params[:option].nil?
|
||||
render status: 400, json: false
|
||||
return
|
||||
end
|
||||
|
||||
post = Post.find(params[:post_id])
|
||||
poll = PollPlugin::Poll.new(post)
|
||||
unless poll.has_poll_details?
|
||||
render status: 400, json: false
|
||||
return
|
||||
end
|
||||
|
||||
options = poll.details
|
||||
|
||||
unless options.keys.include? params[:option]
|
||||
render status: 400, json: false
|
||||
return
|
||||
end
|
||||
|
||||
poll.set_vote!(current_user, params[:option])
|
||||
|
||||
MessageBus.publish("/topic/#{post.topic_id}", {
|
||||
id: post.id,
|
||||
post_number: post.post_number,
|
||||
updated_at: Time.now,
|
||||
type: "revised"
|
||||
},
|
||||
group_ids: post.topic.secure_group_ids
|
||||
)
|
||||
|
||||
render json: poll.serialize(current_user)
|
||||
end
|
||||
|
||||
def toggle_close
|
||||
post = Post.find(params[:post_id])
|
||||
topic = post.topic
|
||||
poll = PollPlugin::Poll.new(post)
|
||||
|
||||
# Make sure the user is allowed to close the poll.
|
||||
Guardian.new(current_user).ensure_can_edit!(topic)
|
||||
|
||||
# Make sure this is actually a poll.
|
||||
unless poll.has_poll_details?
|
||||
render status: 400, json: false
|
||||
return
|
||||
end
|
||||
|
||||
# Make sure the topic is not closed.
|
||||
if topic.closed?
|
||||
render status: 400, json: false
|
||||
return
|
||||
end
|
||||
|
||||
# Modify topic title.
|
||||
I18n.with_locale(topic.user.effective_locale) do
|
||||
if topic.title =~ /^(#{I18n.t('poll.prefix').strip})\s?:/i
|
||||
topic.title = topic.title.gsub(/^(#{I18n.t('poll.prefix').strip})\s?:/i, I18n.t('poll.closed_prefix') + ':')
|
||||
elsif topic.title =~ /^(#{I18n.t('poll.closed_prefix').strip})\s?:/i
|
||||
topic.title = topic.title.gsub(/^(#{I18n.t('poll.closed_prefix').strip})\s?:/i, I18n.t('poll.prefix') + ':')
|
||||
end
|
||||
end
|
||||
|
||||
topic.acting_user = current_user
|
||||
topic.save!
|
||||
|
||||
render json: topic, serializer: BasicTopicSerializer
|
||||
end
|
||||
engine_name PLUGIN_NAME
|
||||
isolate_namespace DiscoursePoll
|
||||
end
|
||||
end
|
||||
|
||||
PollPlugin::Engine.routes.draw do
|
||||
put '/' => 'poll#vote'
|
||||
put '/toggle_close' => 'poll#toggle_close'
|
||||
require_dependency "application_controller"
|
||||
class DiscoursePoll::PollsController < ::ApplicationController
|
||||
requires_plugin PLUGIN_NAME
|
||||
|
||||
before_filter :ensure_logged_in
|
||||
|
||||
def vote
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
options = params.require(:options)
|
||||
user_id = current_user.id
|
||||
|
||||
DistributedMutex.synchronize("#{PLUGIN_NAME}-#{post_id}") do
|
||||
post = Post.find(post_id)
|
||||
|
||||
# topic must be open
|
||||
if post.topic.try(:closed) || post.topic.try(:archived)
|
||||
return render_json_error I18n.t("poll.topic_must_be_open_to_vote")
|
||||
end
|
||||
|
||||
polls = post.custom_fields[POLLS_CUSTOM_FIELD]
|
||||
|
||||
return render_json_error I18n.t("poll.no_polls_associated_with_this_post") if polls.blank?
|
||||
|
||||
poll = polls[poll_name]
|
||||
|
||||
return render_json_error I18n.t("poll.no_poll_with_this_name", name: poll_name) if poll.blank?
|
||||
return render_json_error I18n.t("poll.poll_must_be_open_to_vote") if poll["status"] != "open"
|
||||
|
||||
votes = post.custom_fields["#{VOTES_CUSTOM_FIELD}-#{user_id}"] || {}
|
||||
vote = votes[poll_name] || []
|
||||
|
||||
poll["total_votes"] += 1 if vote.size == 0
|
||||
|
||||
poll["options"].each do |option|
|
||||
option["votes"] -= 1 if vote.include?(option["id"])
|
||||
option["votes"] += 1 if options.include?(option["id"])
|
||||
end
|
||||
|
||||
votes[poll_name] = options
|
||||
|
||||
post.custom_fields[POLLS_CUSTOM_FIELD] = polls
|
||||
post.custom_fields["#{VOTES_CUSTOM_FIELD}-#{user_id}"] = votes
|
||||
post.save_custom_fields
|
||||
|
||||
MessageBus.publish("/polls/#{post_id}", { poll: poll })
|
||||
|
||||
render json: { poll: poll, vote: options }
|
||||
end
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
post_id = params.require(:post_id)
|
||||
poll_name = params.require(:poll_name)
|
||||
status = params.require(:status)
|
||||
|
||||
DistributedMutex.synchronize("#{PLUGIN_NAME}-#{post_id}") do
|
||||
post = Post.find(post_id)
|
||||
|
||||
# either staff member or OP
|
||||
unless current_user.try(:staff?) || current_user.try(:id) == post.user_id
|
||||
return render_json_error I18n.t("poll.only_staff_or_op_can_toggle_status")
|
||||
end
|
||||
|
||||
# topic must be open
|
||||
if post.topic.try(:closed) || post.topic.try(:archived)
|
||||
return render_json_error I18n.t("poll.topic_must_be_open_to_toggle_status")
|
||||
end
|
||||
|
||||
polls = post.custom_fields[POLLS_CUSTOM_FIELD]
|
||||
|
||||
return render_json_error I18n.t("poll.no_polls_associated_with_this_post") if polls.blank?
|
||||
return render_json_error I18n.t("poll.no_poll_with_this_name", name: poll_name) if polls[poll_name].blank?
|
||||
|
||||
polls[poll_name]["status"] = status
|
||||
|
||||
post.custom_fields[POLLS_CUSTOM_FIELD] = polls
|
||||
post.save_custom_fields
|
||||
|
||||
MessageBus.publish("/polls/#{post_id}", { poll: polls[poll_name] })
|
||||
|
||||
render json: { poll: polls[poll_name] }
|
||||
end
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
DiscoursePoll::Engine.routes.draw do
|
||||
put "/vote" => "polls#vote"
|
||||
put "/toggle_status" => "polls#toggle_status"
|
||||
end
|
||||
|
||||
Discourse::Application.routes.append do
|
||||
mount ::PollPlugin::Engine, at: '/poll'
|
||||
mount ::DiscoursePoll::Engine, at: "/polls"
|
||||
end
|
||||
|
||||
# Starting a topic title with "Poll:" will create a poll topic. If the title
|
||||
# starts with "poll:" but the first post doesn't contain a list of options in
|
||||
# it we need to raise an error.
|
||||
Post.class_eval do
|
||||
validate :poll_options
|
||||
def poll_options
|
||||
poll = PollPlugin::Poll.new(self)
|
||||
attr_accessor :polls
|
||||
|
||||
return unless poll.is_poll?
|
||||
# save the polls when the post is created
|
||||
after_save do
|
||||
next if self.polls.blank? || !self.polls.is_a?(Hash)
|
||||
|
||||
if poll.options.length == 0
|
||||
self.errors.add(:raw, I18n.t('poll.must_contain_poll_options'))
|
||||
post = self
|
||||
polls = self.polls
|
||||
|
||||
DistributedMutex.synchronize("#{PLUGIN_NAME}-#{post.id}") do
|
||||
post.custom_fields[POLLS_CUSTOM_FIELD] = polls
|
||||
post.save_custom_fields
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
DATA_PREFIX ||= "data-poll-".freeze
|
||||
DEFAULT_POLL_NAME ||= "poll".freeze
|
||||
|
||||
validate(:post, :polls) do
|
||||
# only care when raw has changed!
|
||||
return unless self.raw_changed?
|
||||
|
||||
# TODO: we should fix the callback mess so that the cooked version is available
|
||||
# in the validators instead of cooking twice
|
||||
cooked = PrettyText.cook(self.raw, topic_id: self.topic_id)
|
||||
parsed = Nokogiri::HTML(cooked)
|
||||
|
||||
polls = {}
|
||||
extracted_polls = []
|
||||
|
||||
# extract polls
|
||||
parsed.css("div.poll").each do |p|
|
||||
poll = { "options" => [], "total_votes" => 0 }
|
||||
|
||||
# extract attributes
|
||||
p.attributes.values.each do |attribute|
|
||||
if attribute.name.start_with?(DATA_PREFIX)
|
||||
poll[attribute.name[DATA_PREFIX.length..-1]] = attribute.value
|
||||
end
|
||||
end
|
||||
|
||||
poll.ensure_can_be_edited!
|
||||
# extract options
|
||||
p.css("li[#{DATA_PREFIX}option-id]").each do |o|
|
||||
option_id = o.attributes[DATA_PREFIX + "option-id"].value
|
||||
poll["options"] << { "id" => option_id, "html" => o.inner_html, "votes" => 0 }
|
||||
end
|
||||
|
||||
# add the poll
|
||||
extracted_polls << poll
|
||||
end
|
||||
|
||||
# validate polls
|
||||
extracted_polls.each do |poll|
|
||||
# polls should have a unique name
|
||||
if polls.has_key?(poll["name"])
|
||||
poll["name"] == DEFAULT_POLL_NAME ?
|
||||
self.errors.add(:base, I18n.t("poll.multiple_polls_without_name")) :
|
||||
self.errors.add(:base, I18n.t("poll.multiple_polls_with_same_name", name: poll["name"]))
|
||||
return
|
||||
end
|
||||
|
||||
# options must be unique
|
||||
if poll["options"].map { |o| o["id"] }.uniq.size != poll["options"].size
|
||||
poll["name"] == DEFAULT_POLL_NAME ?
|
||||
self.errors.add(:base, I18n.t("poll.default_poll_must_have_different_options")) :
|
||||
self.errors.add(:base, I18n.t("poll.named_poll_must_have_different_options", name: poll["name"]))
|
||||
return
|
||||
end
|
||||
|
||||
# at least 2 options
|
||||
if poll["options"].size < 2
|
||||
poll["name"] == DEFAULT_POLL_NAME ?
|
||||
self.errors.add(:base, I18n.t("poll.default_poll_must_have_at_least_2_options")) :
|
||||
self.errors.add(:base, I18n.t("poll.named_poll_must_have_at_least_2_options", name: poll["name"]))
|
||||
return
|
||||
end
|
||||
|
||||
# store the valid poll
|
||||
polls[poll["name"]] = poll
|
||||
end
|
||||
|
||||
# are we updating a post outside the 5-minute edit window?
|
||||
if self.id.present? && self.created_at < 5.minutes.ago
|
||||
post = self
|
||||
DistributedMutex.synchronize("#{PLUGIN_NAME}-#{post.id}") do
|
||||
# load previous polls
|
||||
previous_polls = post.custom_fields[POLLS_CUSTOM_FIELD] || {}
|
||||
|
||||
# are the polls different?
|
||||
if polls.keys != previous_polls.keys ||
|
||||
polls.values.map { |p| p["options"] } != previous_polls.values.map { |p| p["options"] }
|
||||
|
||||
# cannot add/remove/change/re-order polls
|
||||
if polls.keys != previous_polls.keys
|
||||
post.errors.add(:base, I18n.t("poll.cannot_change_polls_after_5_minutes"))
|
||||
return
|
||||
end
|
||||
|
||||
# deal with option changes
|
||||
if User.staff.pluck(:id).include?(post.last_editor_id)
|
||||
# staff can only edit options
|
||||
polls.each_key do |poll_name|
|
||||
if polls[poll_name]["options"].size != previous_polls[poll_name]["options"].size
|
||||
post.errors.add(:base, I18n.t("poll.staff_cannot_add_or_remove_options_after_5_minutes"))
|
||||
return
|
||||
end
|
||||
end
|
||||
# merge votes
|
||||
polls.each_key do |poll_name|
|
||||
polls[poll_name]["total_votes"] = previous_polls[poll_name]["total_votes"]
|
||||
for o in 0...polls[poll_name]["options"].size
|
||||
polls[poll_name]["options"][o]["votes"] = previous_polls[poll_name]["options"][o]["votes"]
|
||||
end
|
||||
end
|
||||
else
|
||||
# OP cannot change polls after 5 minutes
|
||||
post.errors.add(:base, I18n.t("poll.cannot_change_polls_after_5_minutes"))
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
# immediately store the polls
|
||||
post.custom_fields[POLLS_CUSTOM_FIELD] = polls
|
||||
post.save_custom_fields
|
||||
end
|
||||
else
|
||||
# polls will be saved once we have a post id
|
||||
self.polls = polls
|
||||
end
|
||||
end
|
||||
|
||||
# Save the list of options to PluginStore after the post is saved.
|
||||
Post.class_eval do
|
||||
after_save :save_poll_options_to_plugin_store
|
||||
def save_poll_options_to_plugin_store
|
||||
PollPlugin::Poll.new(self).update_options!
|
||||
end
|
||||
Post.register_custom_field_type(POLLS_CUSTOM_FIELD, :json)
|
||||
Post.register_custom_field_type("#{VOTES_CUSTOM_FIELD}-*", :json)
|
||||
|
||||
TopicView.add_post_custom_fields_whitelister do |user|
|
||||
whitelisted = [POLLS_CUSTOM_FIELD]
|
||||
whitelisted << "#{VOTES_CUSTOM_FIELD}-#{user.id}" if user
|
||||
whitelisted
|
||||
end
|
||||
|
||||
# Add poll details into the post serializer.
|
||||
PostSerializer.class_eval do
|
||||
attributes :poll_details
|
||||
def poll_details
|
||||
PollPlugin::Poll.new(object).serialize(scope.user)
|
||||
end
|
||||
def include_poll_details?
|
||||
PollPlugin::Poll.new(object).has_poll_details?
|
||||
end
|
||||
end
|
||||
add_to_serializer(:post, :polls, false) { post_custom_fields[POLLS_CUSTOM_FIELD] }
|
||||
add_to_serializer(:post, :include_polls?) { post_custom_fields.present? && post_custom_fields[POLLS_CUSTOM_FIELD].present? }
|
||||
|
||||
add_to_serializer(:post, :polls_votes, false) { post_custom_fields["#{VOTES_CUSTOM_FIELD}-#{scope.user.id}"] }
|
||||
add_to_serializer(:post, :include_polls_votes?) { scope.user && post_custom_fields.present? && post_custom_fields["#{VOTES_CUSTOM_FIELD}-#{scope.user.id}"].present? }
|
||||
end
|
||||
|
||||
# Poll UI.
|
||||
register_asset "javascripts/models/poll.js.es6"
|
||||
register_asset "javascripts/controllers/poll.js.es6"
|
||||
register_asset "javascripts/views/poll.js.es6"
|
||||
register_asset "javascripts/discourse/templates/poll.hbs"
|
||||
register_asset "javascripts/initializers/poll.js.es6"
|
||||
register_asset "javascripts/poll_bbcode.js", :server_side
|
||||
|
||||
register_css <<CSS
|
||||
|
||||
.poll-ui table {
|
||||
margin-bottom: 5px;
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.poll-ui tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.poll-ui .row {
|
||||
padding-left: 15px;
|
||||
padding-top: 10px;
|
||||
}
|
||||
|
||||
|
||||
.poll-ui td.radio input {
|
||||
margin-left: -10px !important;
|
||||
}
|
||||
|
||||
.poll-ui td {
|
||||
padding: 4px 8px;
|
||||
}
|
||||
|
||||
.poll-ui td.option .option {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.poll-ui td.option .result {
|
||||
float: right;
|
||||
margin-left: 50px;
|
||||
}
|
||||
|
||||
.poll-ui tr.active {
|
||||
background-color: #FFFFB3;
|
||||
}
|
||||
|
||||
.poll-ui button i.fa {
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
.poll-ui .radio {
|
||||
margin-right: 0px;
|
||||
}
|
||||
|
||||
.poll-ui .toggle-poll {
|
||||
float: right;
|
||||
}
|
||||
|
||||
CSS
|
||||
|
||||
@@ -1,168 +0,0 @@
|
||||
module ::PollPlugin
|
||||
|
||||
class Poll
|
||||
def initialize(post)
|
||||
@post = post
|
||||
end
|
||||
|
||||
def is_poll?
|
||||
# Not a new post, and also not the first post.
|
||||
return false if @post.post_number.present? && @post.post_number > 1
|
||||
|
||||
topic = @post.topic
|
||||
|
||||
# Topic is not set in a couple of cases in the Discourse test suite.
|
||||
return false if topic.nil? || topic.user.nil?
|
||||
|
||||
# New post, but not the first post in the topic.
|
||||
return false if @post.post_number.nil? && topic.highest_post_number > 0
|
||||
|
||||
I18n.with_locale(topic.user.effective_locale) do
|
||||
topic.title =~ /^(#{I18n.t('poll.prefix').strip}|#{I18n.t('poll.closed_prefix').strip})\s?:/i
|
||||
end
|
||||
end
|
||||
|
||||
def has_poll_details?
|
||||
self.is_poll?
|
||||
end
|
||||
|
||||
# Called during validation of poll posts. Discourse already restricts edits to
|
||||
# the OP and staff, we want to make sure that:
|
||||
#
|
||||
# * OP cannot edit options after 5 minutes.
|
||||
# * Staff can only edit options after 5 minutes, not add/remove.
|
||||
def ensure_can_be_edited!
|
||||
# Return if this is a new post or the options were not modified.
|
||||
return if @post.id.nil? || (options.sort == details.keys.sort)
|
||||
|
||||
# First 5 minutes -- allow any modification.
|
||||
return unless @post.created_at < 5.minutes.ago
|
||||
|
||||
if User.find(@post.last_editor_id).staff?
|
||||
# Allow editing options, but not adding or removing.
|
||||
if options.length != details.keys.length
|
||||
@post.errors.add(:poll_options, I18n.t('poll.cannot_add_or_remove_options'))
|
||||
end
|
||||
else
|
||||
# not staff, tell them to contact one.
|
||||
@post.errors.add(:poll_options, I18n.t('poll.cannot_have_modified_options'))
|
||||
end
|
||||
end
|
||||
|
||||
def is_closed?
|
||||
topic = @post.topic
|
||||
topic.closed? || topic.archived? || (topic.title =~ /^#{I18n.t('poll.closed_prefix', locale: topic.user.effective_locale)}/i) === 0
|
||||
end
|
||||
|
||||
def options
|
||||
cooked = PrettyText.cook(@post.raw, topic_id: @post.topic_id)
|
||||
parsed = Nokogiri::HTML(cooked)
|
||||
poll_list = parsed.css(".poll-ui ul").first || parsed.css("ul").first
|
||||
if poll_list
|
||||
poll_list.css("li").map {|x| x.children.to_s.strip }.uniq
|
||||
else
|
||||
[]
|
||||
end
|
||||
end
|
||||
|
||||
def update_options!
|
||||
return unless self.is_poll?
|
||||
return if details && details.keys.sort == options.sort
|
||||
|
||||
if details.try(:length) == options.length
|
||||
|
||||
# Assume only renaming, no reordering. Preserve votes.
|
||||
old_details = self.details
|
||||
old_options = old_details.keys
|
||||
new_details = {}
|
||||
new_options = self.options
|
||||
rename = {}
|
||||
|
||||
0.upto(options.length-1) do |i|
|
||||
new_details[ new_options[i] ] = old_details[ old_options[i] ]
|
||||
|
||||
if new_options[i] != old_options[i]
|
||||
rename[ old_options[i] ] = new_options[i]
|
||||
end
|
||||
end
|
||||
self.set_details! new_details
|
||||
|
||||
# Update existing user votes.
|
||||
# Accessing PluginStoreRow directly isn't a very nice approach but there's
|
||||
# no way around it unfortunately.
|
||||
# TODO: Probably want to move this to a background job.
|
||||
PluginStoreRow.where(plugin_name: "poll", value: rename.keys).where('key LIKE ?', vote_key_prefix+"%").find_each do |row|
|
||||
# This could've been done more efficiently using `update_all` instead of
|
||||
# iterating over each individual vote, however this will be needed in the
|
||||
# future once we support multiple choice polls.
|
||||
row.value = rename[ row.value ]
|
||||
row.save
|
||||
end
|
||||
|
||||
else
|
||||
|
||||
# Options were added or removed.
|
||||
new_options = self.options
|
||||
new_details = self.details || {}
|
||||
new_details.each do |key, value|
|
||||
unless new_options.include? key
|
||||
new_details.delete(key)
|
||||
end
|
||||
end
|
||||
new_options.each do |key|
|
||||
new_details[key] ||= 0
|
||||
end
|
||||
self.set_details! new_details
|
||||
|
||||
end
|
||||
end
|
||||
|
||||
def details
|
||||
@details ||= ::PluginStore.get("poll", details_key)
|
||||
end
|
||||
|
||||
def set_details!(new_details)
|
||||
::PluginStore.set("poll", details_key, new_details)
|
||||
@details = new_details
|
||||
end
|
||||
|
||||
def get_vote(user)
|
||||
user.nil? ? nil : ::PluginStore.get("poll", vote_key(user))
|
||||
end
|
||||
|
||||
def set_vote!(user, option)
|
||||
return if is_closed?
|
||||
|
||||
# Get the user's current vote.
|
||||
DistributedMutex.new(details_key).synchronize do
|
||||
vote = get_vote(user)
|
||||
vote = nil unless details.keys.include? vote
|
||||
|
||||
new_details = details.dup
|
||||
new_details[vote] -= 1 if vote
|
||||
new_details[option] += 1
|
||||
|
||||
::PluginStore.set("poll", vote_key(user), option)
|
||||
set_details! new_details
|
||||
end
|
||||
end
|
||||
|
||||
def serialize(user)
|
||||
return nil if details.nil?
|
||||
{options: details, selected: get_vote(user), closed: is_closed?}
|
||||
end
|
||||
|
||||
private
|
||||
def details_key
|
||||
"poll_options_#{@post.id}"
|
||||
end
|
||||
|
||||
def vote_key_prefix
|
||||
"poll_vote_#{@post.id}_"
|
||||
end
|
||||
|
||||
def vote_key(user)
|
||||
"#{vote_key_prefix}#{user.id}"
|
||||
end
|
||||
end
|
||||
end
|
||||
99
plugins/poll/spec/controllers/polls_controller_spec.rb
Normal file
99
plugins/poll/spec/controllers/polls_controller_spec.rb
Normal file
@@ -0,0 +1,99 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe ::DiscoursePoll::PollsController do
|
||||
routes { ::DiscoursePoll::Engine.routes }
|
||||
|
||||
let!(:user) { log_in }
|
||||
let(:topic) { Fabricate(:topic) }
|
||||
let(:poll) { Fabricate(:post, topic_id: topic.id, user_id: user.id, raw: "[poll]\n- A\n- B\n[/poll]") }
|
||||
|
||||
describe "#vote" do
|
||||
|
||||
it "works" do
|
||||
MessageBus.expects(:publish)
|
||||
|
||||
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["A"] }
|
||||
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["poll"]["name"]).to eq("poll")
|
||||
expect(json["poll"]["total_votes"]).to eq(1)
|
||||
expect(json["vote"]).to eq(["A"])
|
||||
end
|
||||
|
||||
it "supports vote changes" do
|
||||
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["5c24fc1df56d764b550ceae1b9319125"] }
|
||||
expect(response).to be_success
|
||||
|
||||
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["e89dec30bbd9bf50fabf6a05b4324edf"] }
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["poll"]["total_votes"]).to eq(1)
|
||||
expect(json["poll"]["options"][0]["votes"]).to eq(0)
|
||||
expect(json["poll"]["options"][1]["votes"]).to eq(1)
|
||||
end
|
||||
|
||||
it "ensures topic is not closed" do
|
||||
topic.update_attribute(:closed, true)
|
||||
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["A"] }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.topic_must_be_open_to_vote"))
|
||||
end
|
||||
|
||||
it "ensures topic is not archived" do
|
||||
topic.update_attribute(:archived, true)
|
||||
xhr :put, :vote, { post_id: poll.id, poll_name: "poll", options: ["A"] }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.topic_must_be_open_to_vote"))
|
||||
end
|
||||
|
||||
it "ensures polls are associated with the post" do
|
||||
xhr :put, :vote, { post_id: Fabricate(:post).id, poll_name: "foobar", options: ["A"] }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.no_polls_associated_with_this_post"))
|
||||
end
|
||||
|
||||
it "checks the name of the poll" do
|
||||
xhr :put, :vote, { post_id: poll.id, poll_name: "foobar", options: ["A"] }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.no_poll_with_this_name", name: "foobar"))
|
||||
end
|
||||
|
||||
it "ensures poll is open" do
|
||||
closed_poll = Fabricate(:post, raw: "[poll status=closed]\n- A\n- B\n[/poll]")
|
||||
xhr :put, :vote, { post_id: closed_poll.id, poll_name: "poll", options: ["A"] }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.poll_must_be_open_to_vote"))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "#toggle_status" do
|
||||
|
||||
it "works for OP" do
|
||||
MessageBus.expects(:publish)
|
||||
|
||||
xhr :put, :toggle_status, { post_id: poll.id, poll_name: "poll", status: "closed" }
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["poll"]["status"]).to eq("closed")
|
||||
end
|
||||
|
||||
it "works for staff" do
|
||||
log_in(:moderator)
|
||||
MessageBus.expects(:publish)
|
||||
|
||||
xhr :put, :toggle_status, { post_id: poll.id, poll_name: "poll", status: "closed" }
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["poll"]["status"]).to eq("closed")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
137
plugins/poll/spec/controllers/posts_controller_spec.rb
Normal file
137
plugins/poll/spec/controllers/posts_controller_spec.rb
Normal file
@@ -0,0 +1,137 @@
|
||||
require "spec_helper"
|
||||
|
||||
describe PostsController do
|
||||
let!(:user) { log_in }
|
||||
let!(:title) { "Testing Poll Plugin" }
|
||||
|
||||
describe "polls" do
|
||||
|
||||
it "works" do
|
||||
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["cooked"]).to match("data-poll-")
|
||||
expect(json["polls"]["poll"]).to be
|
||||
end
|
||||
|
||||
it "works on any post" do
|
||||
post = Fabricate(:post)
|
||||
xhr :post, :create, { topic_id: post.topic.id, raw: "[poll]\n- A\n- B\n[/poll]" }
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["cooked"]).to match("data-poll-")
|
||||
expect(json["polls"]["poll"]).to be
|
||||
end
|
||||
|
||||
it "should have different options" do
|
||||
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- A[/poll]" }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_must_have_different_options"))
|
||||
end
|
||||
|
||||
it "should have at least 2 options" do
|
||||
xhr :post, :create, { title: title, raw: "[poll]\n- A[/poll]" }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.default_poll_must_have_at_least_2_options"))
|
||||
end
|
||||
|
||||
describe "edit window" do
|
||||
|
||||
describe "within the first 5 minutes" do
|
||||
|
||||
let(:post_id) do
|
||||
Timecop.freeze(3.minutes.ago) do
|
||||
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }
|
||||
::JSON.parse(response.body)["id"]
|
||||
end
|
||||
end
|
||||
|
||||
it "can be changed" do
|
||||
xhr :put, :update, { id: post_id, post: { raw: "[poll]\n- A\n- B\n- C\n[/poll]" } }
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["post"]["polls"]["poll"]["options"][2]["html"]).to eq("C")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "after the first 5 minutes" do
|
||||
|
||||
let(:post_id) do
|
||||
Timecop.freeze(6.minutes.ago) do
|
||||
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]" }
|
||||
::JSON.parse(response.body)["id"]
|
||||
end
|
||||
end
|
||||
|
||||
let(:new_raw) { "[poll]\n- A\n- C[/poll]" }
|
||||
|
||||
it "cannot be changed by OP" do
|
||||
xhr :put, :update, { id: post_id, post: { raw: new_raw } }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.cannot_change_polls_after_5_minutes"))
|
||||
end
|
||||
|
||||
it "can be edited by staff" do
|
||||
log_in_user(Fabricate(:moderator))
|
||||
xhr :put, :update, { id: post_id, post: { raw: new_raw } }
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["post"]["polls"]["poll"]["options"][1]["html"]).to eq("C")
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "named polls" do
|
||||
|
||||
it "should have different options" do
|
||||
xhr :post, :create, { title: title, raw: "[poll name=foo]\n- A\n- A[/poll]" }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_different_options", name: "foo"))
|
||||
end
|
||||
|
||||
it "should have at least 2 options" do
|
||||
xhr :post, :create, { title: title, raw: "[poll name=foo]\n- A[/poll]" }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.named_poll_must_have_at_least_2_options", name: "foo"))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
describe "multiple polls" do
|
||||
|
||||
it "works" do
|
||||
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" }
|
||||
expect(response).to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["cooked"]).to match("data-poll-")
|
||||
expect(json["polls"]["poll"]).to be
|
||||
expect(json["polls"]["foo"]).to be
|
||||
end
|
||||
|
||||
it "should have a name" do
|
||||
xhr :post, :create, { title: title, raw: "[poll]\n- A\n- B\n[/poll]\n[poll]\n- A\n- B\n[/poll]" }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.multiple_polls_without_name"))
|
||||
end
|
||||
|
||||
it "should have unique name" do
|
||||
xhr :post, :create, { title: title, raw: "[poll name=foo]\n- A\n- B\n[/poll]\n[poll name=foo]\n- A\n- B\n[/poll]" }
|
||||
expect(response).not_to be_success
|
||||
json = ::JSON.parse(response.body)
|
||||
expect(json["errors"][0]).to eq(I18n.t("poll.multiple_polls_with_same_name", name: "foo"))
|
||||
end
|
||||
|
||||
end
|
||||
|
||||
end
|
||||
@@ -1,92 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe PollPlugin::PollController, type: :controller do
|
||||
routes { PollPlugin::Engine.routes }
|
||||
|
||||
let(:topic) { create_topic(title: "Poll: Chitoge vs Onodera") }
|
||||
let!(:post) { create_post(topic: topic, raw: "Pick one.\n\n[poll]\n* Chitoge\n* Onodera\n[/poll]") }
|
||||
let(:user1) { Fabricate(:user) }
|
||||
let(:user2) { Fabricate(:user) }
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
|
||||
describe 'vote' do
|
||||
it "returns 403 if no user is logged in" do
|
||||
xhr :put, :vote, post_id: post.id, option: "Chitoge"
|
||||
response.should be_forbidden
|
||||
end
|
||||
|
||||
it "returns 400 if post_id or invalid option is not specified" do
|
||||
log_in_user user1
|
||||
xhr :put, :vote
|
||||
response.status.should eq(400)
|
||||
xhr :put, :vote, post_id: post.id
|
||||
response.status.should eq(400)
|
||||
xhr :put, :vote, option: "Chitoge"
|
||||
response.status.should eq(400)
|
||||
xhr :put, :vote, post_id: post.id, option: "Tsugumi"
|
||||
response.status.should eq(400)
|
||||
end
|
||||
|
||||
it "returns 400 if post_id doesn't correspond to a poll post" do
|
||||
log_in_user user1
|
||||
post2 = create_post(topic: topic, raw: "Generic reply")
|
||||
xhr :put, :vote, post_id: post2.id, option: "Chitoge"
|
||||
end
|
||||
|
||||
it "saves votes correctly" do
|
||||
MessageBus.expects(:publish).times(3)
|
||||
|
||||
log_in_user user1
|
||||
xhr :put, :vote, post_id: post.id, option: "Chitoge"
|
||||
PollPlugin::Poll.new(post).get_vote(user1).should eq("Chitoge")
|
||||
|
||||
log_in_user user2
|
||||
xhr :put, :vote, post_id: post.id, option: "Onodera"
|
||||
PollPlugin::Poll.new(post).get_vote(user2).should eq("Onodera")
|
||||
|
||||
PollPlugin::Poll.new(post).details["Chitoge"].should eq(1)
|
||||
PollPlugin::Poll.new(post).details["Onodera"].should eq(1)
|
||||
|
||||
xhr :put, :vote, post_id: post.id, option: "Chitoge"
|
||||
PollPlugin::Poll.new(post).get_vote(user2).should eq("Chitoge")
|
||||
|
||||
PollPlugin::Poll.new(post).details["Chitoge"].should eq(2)
|
||||
PollPlugin::Poll.new(post).details["Onodera"].should eq(0)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'toggle_close' do
|
||||
it "returns 400 if post_id doesn't correspond to a poll post" do
|
||||
log_in_user admin
|
||||
post2 = create_post(topic: topic, raw: "Generic reply")
|
||||
xhr :put, :toggle_close, post_id: post2.id
|
||||
response.status.should eq(400)
|
||||
end
|
||||
|
||||
it "returns 400 if the topic is locked" do
|
||||
log_in_user admin
|
||||
topic.update_attributes closed: true
|
||||
xhr :put, :toggle_close, post_id: post.id
|
||||
response.status.should eq(400)
|
||||
end
|
||||
|
||||
it "raises Discourse::InvalidAccess is the user is not authorized" do
|
||||
log_in_user user1
|
||||
expect do
|
||||
xhr :put, :toggle_close, post_id: post.id
|
||||
end.to raise_error(Discourse::InvalidAccess)
|
||||
end
|
||||
|
||||
it "renames the topic" do
|
||||
I18n.stubs(:t).with('poll.prefix').returns("Poll ")
|
||||
I18n.stubs(:t).with('poll.closed_prefix').returns("Closed Poll ")
|
||||
log_in_user admin
|
||||
xhr :put, :toggle_close, post_id: post.id
|
||||
response.status.should eq(200)
|
||||
topic.reload.title.should == "Closed Poll : Chitoge vs Onodera"
|
||||
xhr :put, :toggle_close, post_id: post.id
|
||||
response.status.should eq(200)
|
||||
topic.reload.title.should == "Poll : Chitoge vs Onodera"
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -1,97 +0,0 @@
|
||||
require 'spec_helper'
|
||||
|
||||
describe PollPlugin::Poll do
|
||||
let(:topic) { create_topic(title: "Poll: Chitoge vs Onodera") }
|
||||
let(:post) { create_post(topic: topic, raw: "Pick one.\n\n[poll]\n* Chitoge\n* Onodera\n[/poll]") }
|
||||
let(:poll) { PollPlugin::Poll.new(post) }
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
it "should detect poll post correctly" do
|
||||
expect(poll.is_poll?).to be_truthy
|
||||
post2 = create_post(topic: topic, raw: "This is a generic reply.")
|
||||
expect(PollPlugin::Poll.new(post2).is_poll?).to be_falsey
|
||||
post.topic.title = "Not a poll"
|
||||
expect(poll.is_poll?).to be_falsey
|
||||
end
|
||||
|
||||
it "strips whitespace from the prefix translation" do
|
||||
topic.title = "Polll: This might be a poll"
|
||||
topic.save
|
||||
expect(PollPlugin::Poll.new(post).is_poll?).to be_falsey
|
||||
I18n.expects(:t).with('poll.prefix').returns("Polll ")
|
||||
I18n.expects(:t).with('poll.closed_prefix').returns("Closed Poll ")
|
||||
expect(PollPlugin::Poll.new(post).is_poll?).to be_truthy
|
||||
end
|
||||
|
||||
it "should get options correctly" do
|
||||
expect(poll.options).to eq(["Chitoge", "Onodera"])
|
||||
end
|
||||
|
||||
it "should fall back to using the first list if [poll] markup is not present" do
|
||||
topic = create_topic(title: "This is not a poll topic")
|
||||
post = create_post(topic: topic, raw: "Pick one.\n\n* Chitoge\n* Onodera")
|
||||
poll = PollPlugin::Poll.new(post)
|
||||
expect(poll.options).to eq(["Chitoge", "Onodera"])
|
||||
end
|
||||
|
||||
it "should get details correctly" do
|
||||
expect(poll.details).to eq({"Chitoge" => 0, "Onodera" => 0})
|
||||
end
|
||||
|
||||
it "should set details correctly" do
|
||||
poll.set_details!({})
|
||||
poll.details.should eq({})
|
||||
PollPlugin::Poll.new(post).details.should eq({})
|
||||
end
|
||||
|
||||
it "should get and set votes correctly" do
|
||||
poll.get_vote(user).should eq(nil)
|
||||
poll.set_vote!(user, "Onodera")
|
||||
poll.get_vote(user).should eq("Onodera")
|
||||
poll.details["Onodera"].should eq(1)
|
||||
end
|
||||
|
||||
it "should not set votes on closed polls" do
|
||||
poll.set_vote!(user, "Onodera")
|
||||
post.topic.closed = true
|
||||
post.topic.save!
|
||||
poll.set_vote!(user, "Chitoge")
|
||||
poll.get_vote(user).should eq("Onodera")
|
||||
end
|
||||
|
||||
it "should serialize correctly" do
|
||||
poll.serialize(user).should eq({options: poll.details, selected: nil, closed: false})
|
||||
poll.set_vote!(user, "Onodera")
|
||||
poll.serialize(user).should eq({options: poll.details, selected: "Onodera", closed: false})
|
||||
poll.serialize(nil).should eq({options: poll.details, selected: nil, closed: false})
|
||||
|
||||
topic.title = "Closed Poll: my poll"
|
||||
topic.save
|
||||
|
||||
post.topic.reload
|
||||
poll = PollPlugin::Poll.new(post)
|
||||
poll.serialize(nil).should eq({options: poll.details, selected: nil, closed: true})
|
||||
end
|
||||
|
||||
it "should serialize to nil if there are no poll options" do
|
||||
topic = create_topic(title: "This is not a poll topic")
|
||||
post = create_post(topic: topic, raw: "no options in the content")
|
||||
poll = PollPlugin::Poll.new(post)
|
||||
poll.serialize(user).should eq(nil)
|
||||
end
|
||||
|
||||
it "stores poll options to plugin store" do
|
||||
poll.set_vote!(user, "Onodera")
|
||||
poll.stubs(:options).returns(["Chitoge", "Onodera", "Inferno Cop"])
|
||||
poll.update_options!
|
||||
poll.details.keys.sort.should eq(["Chitoge", "Inferno Cop", "Onodera"])
|
||||
poll.details["Inferno Cop"].should eq(0)
|
||||
poll.details["Onodera"].should eq(1)
|
||||
|
||||
poll.stubs(:options).returns(["Chitoge", "Onodera v2", "Inferno Cop"])
|
||||
poll.update_options!
|
||||
poll.details.keys.sort.should eq(["Chitoge", "Inferno Cop", "Onodera v2"])
|
||||
poll.details["Onodera v2"].should eq(1)
|
||||
poll.get_vote(user).should eq("Onodera v2")
|
||||
end
|
||||
end
|
||||
@@ -1,37 +0,0 @@
|
||||
require 'spec_helper'
|
||||
require 'post_creator'
|
||||
|
||||
describe PostCreator do
|
||||
let(:user) { Fabricate(:user) }
|
||||
let(:admin) { Fabricate(:admin) }
|
||||
|
||||
context "poll topic" do
|
||||
let(:poll_post) { PostCreator.create(user, {title: "Poll: This is a poll", raw: "[poll]\n* option 1\n* option 2\n* option 3\n* option 4\n[/poll]"}) }
|
||||
|
||||
it "cannot be created without a list of options" do
|
||||
post = PostCreator.create(user, {title: "Poll: This is a poll", raw: "body does not contain a list"})
|
||||
post.errors[:raw].should be_present
|
||||
end
|
||||
|
||||
it "cannot have options changed after 5 minutes" do
|
||||
poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]"
|
||||
poll_post.valid?.should == true
|
||||
poll_post.save
|
||||
Timecop.freeze(Time.now + 6.minutes) do
|
||||
poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n* option 4\n[/poll]"
|
||||
poll_post.valid?.should == false
|
||||
poll_post.errors[:poll_options].should be_present
|
||||
end
|
||||
end
|
||||
|
||||
it "allows staff to edit options after 5 minutes" do
|
||||
poll_post.last_editor_id = admin.id
|
||||
Timecop.freeze(Time.now + 6.minutes) do
|
||||
poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n* option 4.1\n[/poll]"
|
||||
poll_post.valid?.should == true
|
||||
poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]"
|
||||
poll_post.valid?.should == false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Reference in New Issue
Block a user