diff --git a/plugins/poll/assets/javascripts/discourse/templates/poll.js.handlebars b/plugins/poll/assets/javascripts/discourse/templates/poll.js.handlebars index fdaff32268f..3168dc130c0 100644 --- a/plugins/poll/assets/javascripts/discourse/templates/poll.js.handlebars +++ b/plugins/poll/assets/javascripts/discourse/templates/poll.js.handlebars @@ -1,7 +1,7 @@
+ |
{{{ option }}}
diff --git a/plugins/poll/assets/javascripts/poll_ui.js b/plugins/poll/assets/javascripts/poll_ui.js
index fdafc7ec268..aa9ea01dc7d 100644
--- a/plugins/poll/assets/javascripts/poll_ui.js
+++ b/plugins/poll/assets/javascripts/poll_ui.js
@@ -38,8 +38,14 @@ var PollController = Discourse.Controller.extend({
poll: null,
showResults: false,
+ disableRadio: Em.computed.any('poll.post.topic.closed', 'loading'),
+
actions: {
selectOption: function(option) {
+ if (this.get('disableRadio')) {
+ return;
+ }
+
if (!this.get('currentUser.id')) {
this.get('postController').send('showLogin');
return;
diff --git a/plugins/poll/config/locales/server.en.yml b/plugins/poll/config/locales/server.en.yml
index a270072f30b..6e1e8829ad2 100644
--- a/plugins/poll/config/locales/server.en.yml
+++ b/plugins/poll/config/locales/server.en.yml
@@ -5,7 +5,12 @@
# http://yamllint.com/
en:
+ activerecord:
+ attributes:
+ post:
+ poll_options: "Poll options"
poll:
must_contain_poll_options: "must contain a list of poll options"
- cannot_have_modified_options: "cannot have modified poll options after 5 minutes"
+ 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 thread and create a new one."
prefix: "Poll:"
diff --git a/plugins/poll/config/locales/server.fr.yml b/plugins/poll/config/locales/server.fr.yml
index 85bf5b022a1..cf55d08be7e 100644
--- a/plugins/poll/config/locales/server.fr.yml
+++ b/plugins/poll/config/locales/server.fr.yml
@@ -14,4 +14,4 @@ fr:
poll:
must_contain_poll_options: "doit contenir une liste de choix pour le sondage"
cannot_have_modified_options: "ne peut pas être modifié car il contient un sondage publié depuis plus de 5 minutes"
- prefix: "Sondage:"
+ prefix: "Sondage\\s?:"
diff --git a/plugins/poll/plugin.rb b/plugins/poll/plugin.rb
index a959d1bab15..454a6835a28 100644
--- a/plugins/poll/plugin.rb
+++ b/plugins/poll/plugin.rb
@@ -6,8 +6,7 @@
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.
+# because all of this is instance_eval'd inside an instance of Plugin::Instance.
PollPlugin = PollPlugin
after_initialize do
@@ -64,8 +63,6 @@ after_initialize do
# 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
@@ -77,30 +74,15 @@ after_initialize do
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
+ poll.ensure_can_be_edited!
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
+ after_save :save_poll_options_to_plugin_store
+ def save_poll_options_to_plugin_store
+ PollPlugin::Poll.new(self).update_options!
end
end
diff --git a/plugins/poll/poll.rb b/plugins/poll/poll.rb
index 5751e9e4119..c7ebfd2f70d 100644
--- a/plugins/poll/poll.rb
+++ b/plugins/poll/poll.rb
@@ -24,6 +24,29 @@ module ::PollPlugin
topic.title =~ /^#{I18n.t('poll.prefix')}/i
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
+ # Regular user, tell them to contact a moderator.
+ @post.errors.add(:poll_options, I18n.t('poll.cannot_have_modified_options'))
+ end
+ end
+
def options
cooked = PrettyText.cook(@post.raw, topic_id: @post.topic_id)
parsed = Nokogiri::HTML(cooked)
@@ -35,6 +58,58 @@ module ::PollPlugin
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
@@ -49,6 +124,8 @@ module ::PollPlugin
end
def set_vote!(user, option)
+ return if @post.topic.closed?
+
# Get the user's current vote.
vote = get_vote(user)
vote = nil unless details.keys.include? vote
@@ -71,8 +148,12 @@ module ::PollPlugin
"poll_options_#{@post.id}"
end
+ def vote_key_prefix
+ "poll_vote_#{@post.id}_"
+ end
+
def vote_key(user)
- "poll_vote_#{@post.id}_#{user.id}"
+ "#{vote_key_prefix}#{user.id}"
end
end
end
diff --git a/plugins/poll/spec/poll_plugin/poll_spec.rb b/plugins/poll/spec/poll_plugin/poll_spec.rb
index de4333352c8..36c4506ec71 100644
--- a/plugins/poll/spec/poll_plugin/poll_spec.rb
+++ b/plugins/poll/spec/poll_plugin/poll_spec.rb
@@ -50,6 +50,14 @@ describe PollPlugin::Poll do
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})
poll.set_vote!(user, "Onodera")
@@ -63,4 +71,19 @@ describe PollPlugin::Poll do
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
diff --git a/plugins/poll/spec/post_creator_spec.rb b/plugins/poll/spec/post_creator_spec.rb
index 09db32ce2df..7e2c78d2a85 100644
--- a/plugins/poll/spec/post_creator_spec.rb
+++ b/plugins/poll/spec/post_creator_spec.rb
@@ -3,22 +3,34 @@ 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
- 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
+ poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]"
+ poll_post.valid?.should be_true
+ poll_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
+ poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n* option 4\n[/poll]"
+ poll_post.valid?.should be_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 be_true
+ poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]"
+ poll_post.valid?.should be_false
end
end
end
|