mirror of
https://github.com/discourse/discourse.git
synced 2024-11-23 03:40:00 +08:00
commit
634b769cda
|
@ -425,7 +425,8 @@ Discourse.Composer = Discourse.Model.extend({
|
|||
this.set('composeState', CLOSED);
|
||||
|
||||
return Ember.Deferred.promise(function(promise) {
|
||||
post.save(function() {
|
||||
post.save(function(result) {
|
||||
post.updateFromPost(result);
|
||||
composer.clearState();
|
||||
}, function(error) {
|
||||
var response = $.parseJSON(error.responseText);
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
<table>
|
||||
{{#each poll.options}}
|
||||
<tr {{bind-attr class=checked:active}} {{action selectOption option}}>
|
||||
<td class="radio"><input type="radio" name="poll" {{bind-attr checked=checked disabled=controller.loading}}></td>
|
||||
<td class="option">
|
||||
<div class="option">
|
||||
{{option}}
|
||||
</div>
|
||||
{{#if controller.showResults}}
|
||||
<div class="result">{{i18n poll.voteCount count=votes}}</div>
|
||||
{{/if}}
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
|
||||
<button {{action toggleShowResults}}>
|
||||
{{#if showResults}}
|
||||
{{i18n poll.results.hide}}
|
||||
{{else}}
|
||||
{{i18n poll.results.show}}
|
||||
{{/if}}
|
||||
</button>
|
||||
|
||||
{{#if loading}}
|
||||
<i class="fa fa-spin fa-spinner"></i>
|
||||
{{/if}}
|
9
plugins/poll/assets/javascripts/poll_bbcode.js
Normal file
9
plugins/poll/assets/javascripts/poll_bbcode.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
Discourse.Dialect.inlineBetween({
|
||||
start: '[poll]',
|
||||
stop: '[/poll]',
|
||||
rawContents: true,
|
||||
emitter: function(contents) {
|
||||
var list = Discourse.Dialect.cook(contents, {});
|
||||
return ['div', {class: 'poll-ui'}, list];
|
||||
}
|
||||
});
|
110
plugins/poll/assets/javascripts/poll_ui.js
Normal file
110
plugins/poll/assets/javascripts/poll_ui.js
Normal file
|
@ -0,0 +1,110 @@
|
|||
var Poll = Discourse.Model.extend({
|
||||
post: null,
|
||||
options: [],
|
||||
|
||||
postObserver: function() {
|
||||
this.updateOptionsFromJson(this.get('post.poll_details'));
|
||||
}.observes('post.poll_details'),
|
||||
|
||||
updateOptionsFromJson: function(json) {
|
||||
var selectedOption = json["selected"];
|
||||
|
||||
var options = [];
|
||||
Object.keys(json["options"]).forEach(function(option) {
|
||||
options.push(Ember.Object.create({
|
||||
option: option,
|
||||
votes: json["options"][option],
|
||||
checked: (option == selectedOption)
|
||||
}));
|
||||
});
|
||||
this.set('options', options);
|
||||
},
|
||||
|
||||
saveVote: function(option) {
|
||||
this.get('options').forEach(function(opt) {
|
||||
opt.set('checked', opt.get('option') == option);
|
||||
});
|
||||
|
||||
return Discourse.ajax("/poll", {
|
||||
type: "PUT",
|
||||
data: {post_id: this.get('post.id'), option: option}
|
||||
}).then(function(newJSON) {
|
||||
this.updateOptionsFromJson(newJSON);
|
||||
}.bind(this));
|
||||
}
|
||||
});
|
||||
|
||||
var PollController = Discourse.Controller.extend({
|
||||
poll: null,
|
||||
showResults: false,
|
||||
|
||||
actions: {
|
||||
selectOption: function(option) {
|
||||
if (!this.get('currentUser.id')) {
|
||||
this.get('postController').send('showLogin');
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('loading', true);
|
||||
this.get('poll').saveVote(option).then(function() {
|
||||
this.set('loading', false);
|
||||
this.set('showResults', true);
|
||||
}.bind(this));
|
||||
},
|
||||
|
||||
toggleShowResults: function() {
|
||||
this.set('showResults', !this.get('showResults'));
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
var PollView = Ember.View.extend({
|
||||
templateName: "poll",
|
||||
classNames: ['poll-ui'],
|
||||
|
||||
replaceElement: function(target) {
|
||||
this._insertElementLater(function() {
|
||||
target.replaceWith(this.$());
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function initializePollView(self) {
|
||||
var post = self.get('post');
|
||||
var pollDetails = post.get('poll_details');
|
||||
|
||||
var poll = Poll.create({post: post});
|
||||
poll.updateOptionsFromJson(pollDetails);
|
||||
|
||||
var pollController = PollController.create({
|
||||
poll: poll,
|
||||
showResults: pollDetails["selected"],
|
||||
postController: self.get('controller')
|
||||
});
|
||||
|
||||
var pollView = self.createChildView(PollView, {
|
||||
controller: pollController
|
||||
});
|
||||
return pollView;
|
||||
}
|
||||
|
||||
Discourse.PostView.reopen({
|
||||
createPollUI: function($post) {
|
||||
var post = this.get('post');
|
||||
|
||||
if (!post.get('poll_details')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var view = initializePollView(this);
|
||||
view.replaceElement($post.find(".poll-ui:first"));
|
||||
this.set('pollView', view);
|
||||
|
||||
}.on('postViewInserted'),
|
||||
|
||||
clearPollView: function() {
|
||||
if (this.get('pollView')) {
|
||||
this.get('pollView').destroy();
|
||||
}
|
||||
}.on('willClearRender')
|
||||
});
|
17
plugins/poll/config/locales/client.en.yml
Normal file
17
plugins/poll/config/locales/client.en.yml
Normal file
|
@ -0,0 +1,17 @@
|
|||
# encoding: utf-8
|
||||
# This file contains content for the client portion of Discourse, sent out
|
||||
# to the Javascript app.
|
||||
#
|
||||
# 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"
|
||||
|
||||
results:
|
||||
show: Show Results
|
||||
hide: Hide Results
|
11
plugins/poll/config/locales/server.en.yml
Normal file
11
plugins/poll/config/locales/server.en.yml
Normal file
|
@ -0,0 +1,11 @@
|
|||
# encoding: utf-8
|
||||
# This file contains content for the server portion of Discourse used by Ruby
|
||||
#
|
||||
# To validate this YAML file after you change it, please paste it into
|
||||
# http://yamllint.com/
|
||||
|
||||
en:
|
||||
poll:
|
||||
must_contain_poll_options: "must contain a list of poll options"
|
||||
cannot_have_modified_options: "cannot have modified poll options after 5 minutes"
|
||||
prefix: "Poll:"
|
159
plugins/poll/plugin.rb
Normal file
159
plugins/poll/plugin.rb
Normal file
|
@ -0,0 +1,159 @@
|
|||
# name: poll
|
||||
# about: adds poll support to Discourse
|
||||
# version: 0.1
|
||||
# authors: Vikhyat Korrapati
|
||||
|
||||
load File.expand_path("../poll.rb", __FILE__)
|
||||
|
||||
# Without this line we can't lookup the constant inside the after_initialize blocks,
|
||||
# probably because all of this is instance_eval'd inside an instance of
|
||||
# Plugin::Instance.
|
||||
PollPlugin = PollPlugin
|
||||
|
||||
after_initialize do
|
||||
# Rails Engine for accepting votes.
|
||||
module PollPlugin
|
||||
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.is_poll?
|
||||
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])
|
||||
|
||||
render json: poll.serialize(current_user)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
PollPlugin::Engine.routes.draw do
|
||||
put '/' => 'poll#vote'
|
||||
end
|
||||
|
||||
Discourse::Application.routes.append do
|
||||
mount ::PollPlugin::Engine, at: '/poll'
|
||||
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.
|
||||
# Need to add an error when:
|
||||
# * there is no list of options.
|
||||
Post.class_eval do
|
||||
validate :poll_options
|
||||
def poll_options
|
||||
poll = PollPlugin::Poll.new(self)
|
||||
|
||||
return unless poll.is_poll?
|
||||
|
||||
if poll.options.length == 0
|
||||
self.errors.add(:raw, I18n.t('poll.must_contain_poll_options'))
|
||||
end
|
||||
|
||||
if self.created_at and self.created_at < 5.minutes.ago and poll.options.sort != poll.details.keys.sort
|
||||
self.errors.add(:raw, I18n.t('poll.cannot_have_modified_options'))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Save the list of options to PluginStore after the post is saved.
|
||||
Post.class_eval do
|
||||
after_save :save_poll_options_to_topic_metadata
|
||||
def save_poll_options_to_topic_metadata
|
||||
poll = PollPlugin::Poll.new(self)
|
||||
if poll.is_poll?
|
||||
details = poll.details || {}
|
||||
new_options = poll.options
|
||||
details.each do |key, value|
|
||||
unless new_options.include? key
|
||||
details.delete(key)
|
||||
end
|
||||
end
|
||||
new_options.each do |key|
|
||||
details[key] ||= 0
|
||||
end
|
||||
poll.set_details! details
|
||||
end
|
||||
end
|
||||
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).is_poll?
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
# Poll UI.
|
||||
register_asset "javascripts/discourse/templates/poll.js.handlebars"
|
||||
register_asset "javascripts/poll_ui.js"
|
||||
register_asset "javascripts/poll_bbcode.js", :server_side
|
||||
|
||||
register_css <<CSS
|
||||
|
||||
.poll-ui table {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.poll-ui tr {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.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 {
|
||||
border: none;
|
||||
}
|
||||
|
||||
CSS
|
77
plugins/poll/poll.rb
Normal file
77
plugins/poll/poll.rb
Normal file
|
@ -0,0 +1,77 @@
|
|||
module ::PollPlugin
|
||||
|
||||
class Poll
|
||||
def initialize(post)
|
||||
@post = post
|
||||
end
|
||||
|
||||
def is_poll?
|
||||
if !@post.post_number.nil? and @post.post_number > 1
|
||||
# Not a new post, and also not the first post.
|
||||
return false
|
||||
end
|
||||
|
||||
topic = @post.topic
|
||||
|
||||
# Topic is not set in a couple of cases in the Discourse test suite.
|
||||
return false if topic.nil?
|
||||
|
||||
if @post.post_number.nil? and topic.highest_post_number > 0
|
||||
# New post, but not the first post in the topic.
|
||||
return false
|
||||
end
|
||||
|
||||
topic.title =~ /^#{I18n.t('poll.prefix')}/i
|
||||
end
|
||||
|
||||
def options
|
||||
cooked = PrettyText.cook(@post.raw, topic_id: @post.topic_id)
|
||||
poll_div = Nokogiri::HTML(cooked).css(".poll-ui").first
|
||||
if poll_div
|
||||
poll_div.css("li").map {|x| x.children.to_s.strip }.uniq
|
||||
else
|
||||
[]
|
||||
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)
|
||||
# Get the user's current vote.
|
||||
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
|
||||
|
||||
def serialize(user)
|
||||
return nil if details.nil?
|
||||
{options: details, selected: get_vote(user)}
|
||||
end
|
||||
|
||||
private
|
||||
def details_key
|
||||
"poll_options_#{@post.id}"
|
||||
end
|
||||
|
||||
def vote_key(user)
|
||||
"poll_vote_#{@post.id}_#{user.id}"
|
||||
end
|
||||
end
|
||||
end
|
51
plugins/poll/spec/poll_plugin/poll_controller_spec.rb
Normal file
51
plugins/poll/spec/poll_plugin/poll_controller_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
require 'spec_helper'
|
||||
|
||||
describe PollPlugin::PollController, type: :controller 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(:user1) { Fabricate(:user) }
|
||||
let(:user2) { Fabricate(:user) }
|
||||
|
||||
it "should return 403 if no user is logged in" do
|
||||
xhr :put, :vote, post_id: post.id, option: "Chitoge", use_route: :poll
|
||||
response.should be_forbidden
|
||||
end
|
||||
|
||||
it "should return 400 if post_id or invalid option is not specified" do
|
||||
log_in_user user1
|
||||
xhr :put, :vote, use_route: :poll
|
||||
response.status.should eq(400)
|
||||
xhr :put, :vote, post_id: post.id, use_route: :poll
|
||||
response.status.should eq(400)
|
||||
xhr :put, :vote, option: "Chitoge", use_route: :poll
|
||||
response.status.should eq(400)
|
||||
xhr :put, :vote, post_id: post.id, option: "Tsugumi", use_route: :poll
|
||||
response.status.should eq(400)
|
||||
end
|
||||
|
||||
it "should return 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", use_route: :poll
|
||||
response.status.should eq(400)
|
||||
end
|
||||
|
||||
it "should save votes correctly" do
|
||||
log_in_user user1
|
||||
xhr :put, :vote, post_id: post.id, option: "Chitoge", use_route: :poll
|
||||
PollPlugin::Poll.new(post).get_vote(user1).should eq("Chitoge")
|
||||
|
||||
log_in_user user2
|
||||
xhr :put, :vote, post_id: post.id, option: "Onodera", use_route: :poll
|
||||
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", use_route: :poll
|
||||
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
|
51
plugins/poll/spec/poll_plugin/poll_spec.rb
Normal file
51
plugins/poll/spec/poll_plugin/poll_spec.rb
Normal file
|
@ -0,0 +1,51 @@
|
|||
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_true
|
||||
post2 = create_post(topic: topic, raw: "This is a generic reply.")
|
||||
expect(PollPlugin::Poll.new(post2).is_poll?).to be_false
|
||||
post.topic.title = "Not a poll"
|
||||
expect(poll.is_poll?).to be_false
|
||||
end
|
||||
|
||||
it "should get options correctly" do
|
||||
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 serialize correctly" do
|
||||
poll.serialize(user).should eq({options: poll.details, selected: nil})
|
||||
poll.set_vote!(user, "Onodera")
|
||||
poll.serialize(user).should eq({options: poll.details, selected: "Onodera"})
|
||||
poll.serialize(nil).should eq({options: poll.details, selected: nil})
|
||||
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
|
||||
end
|
25
plugins/poll/spec/post_creator_spec.rb
Normal file
25
plugins/poll/spec/post_creator_spec.rb
Normal file
|
@ -0,0 +1,25 @@
|
|||
require 'spec_helper'
|
||||
require 'post_creator'
|
||||
|
||||
describe PostCreator do
|
||||
let(:user) { Fabricate(:user) }
|
||||
|
||||
context "poll topic" do
|
||||
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
|
||||
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]"})
|
||||
post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]"
|
||||
post.valid?.should be_true
|
||||
post.save
|
||||
Timecop.freeze(Time.now + 6.minutes) do
|
||||
post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n* option 4\n[/poll]"
|
||||
post.valid?.should be_false
|
||||
post.errors[:raw].should be_present
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
Loading…
Reference in New Issue
Block a user