Poll Plugin: Allow staff to edit options.

This commit is contained in:
Vikhyat Korrapati 2014-03-16 19:05:47 +05:30
parent e4c793a7e3
commit ae3f135c33
5 changed files with 125 additions and 32 deletions

View file

@ -5,7 +5,12 @@
# http://yamllint.com/ # http://yamllint.com/
en: en:
activerecord:
attributes:
post:
poll_options: "Poll options"
poll: poll:
must_contain_poll_options: "must contain a list of poll options" 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:" prefix: "Poll:"

View file

@ -6,8 +6,7 @@
load File.expand_path("../poll.rb", __FILE__) load File.expand_path("../poll.rb", __FILE__)
# Without this line we can't lookup the constant inside the after_initialize blocks, # 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 # because all of this is instance_eval'd inside an instance of Plugin::Instance.
# Plugin::Instance.
PollPlugin = PollPlugin PollPlugin = PollPlugin
after_initialize do after_initialize do
@ -64,8 +63,6 @@ after_initialize do
# Starting a topic title with "Poll:" will create a poll topic. If the title # 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 # starts with "poll:" but the first post doesn't contain a list of options in
# it we need to raise an error. # it we need to raise an error.
# Need to add an error when:
# * there is no list of options.
Post.class_eval do Post.class_eval do
validate :poll_options validate :poll_options
def poll_options def poll_options
@ -77,30 +74,15 @@ after_initialize do
self.errors.add(:raw, I18n.t('poll.must_contain_poll_options')) self.errors.add(:raw, I18n.t('poll.must_contain_poll_options'))
end end
if self.created_at and self.created_at < 5.minutes.ago and poll.options.sort != poll.details.keys.sort poll.ensure_can_be_edited!
self.errors.add(:raw, I18n.t('poll.cannot_have_modified_options'))
end
end end
end end
# Save the list of options to PluginStore after the post is saved. # Save the list of options to PluginStore after the post is saved.
Post.class_eval do Post.class_eval do
after_save :save_poll_options_to_topic_metadata after_save :save_poll_options_to_plugin_store
def save_poll_options_to_topic_metadata def save_poll_options_to_plugin_store
poll = PollPlugin::Poll.new(self) PollPlugin::Poll.new(self).update_options!
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
end end

View file

@ -24,6 +24,29 @@ module ::PollPlugin
topic.title =~ /^#{I18n.t('poll.prefix')}/i topic.title =~ /^#{I18n.t('poll.prefix')}/i
end 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 def options
cooked = PrettyText.cook(@post.raw, topic_id: @post.topic_id) cooked = PrettyText.cook(@post.raw, topic_id: @post.topic_id)
parsed = Nokogiri::HTML(cooked) parsed = Nokogiri::HTML(cooked)
@ -35,6 +58,58 @@ module ::PollPlugin
end end
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 def details
@details ||= ::PluginStore.get("poll", details_key) @details ||= ::PluginStore.get("poll", details_key)
end end
@ -71,8 +146,12 @@ module ::PollPlugin
"poll_options_#{@post.id}" "poll_options_#{@post.id}"
end end
def vote_key_prefix
"poll_vote_#{@post.id}_"
end
def vote_key(user) def vote_key(user)
"poll_vote_#{@post.id}_#{user.id}" "#{vote_key_prefix}#{user.id}"
end end
end end
end end

View file

@ -63,4 +63,19 @@ describe PollPlugin::Poll do
poll = PollPlugin::Poll.new(post) poll = PollPlugin::Poll.new(post)
poll.serialize(user).should eq(nil) poll.serialize(user).should eq(nil)
end 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 end

View file

@ -3,22 +3,34 @@ require 'post_creator'
describe PostCreator do describe PostCreator do
let(:user) { Fabricate(:user) } let(:user) { Fabricate(:user) }
let(:admin) { Fabricate(:admin) }
context "poll topic" do 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 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 = PostCreator.create(user, {title: "Poll: This is a poll", raw: "body does not contain a list"})
post.errors[:raw].should be_present post.errors[:raw].should be_present
end end
it "cannot have options changed after 5 minutes" do 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]"}) poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]"
post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n[/poll]" poll_post.valid?.should be_true
post.valid?.should be_true poll_post.save
post.save
Timecop.freeze(Time.now + 6.minutes) do Timecop.freeze(Time.now + 6.minutes) do
post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n* option 4\n[/poll]" poll_post.raw = "[poll]\n* option 1\n* option 2\n* option 3\n* option 4\n[/poll]"
post.valid?.should be_false poll_post.valid?.should be_false
post.errors[:raw].should be_present 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 end
end end