diff --git a/plugins/vote/controllers/vote_plugin_admin_controller.rb b/plugins/vote/controllers/vote_plugin_admin_controller.rb
new file mode 100644
index 0000000..3a8cd88
--- /dev/null
+++ b/plugins/vote/controllers/vote_plugin_admin_controller.rb
@@ -0,0 +1,18 @@
+class VotePluginAdminController < AdminController
+
+ def index
+ settings = params[:settings]
+ settings ||= {}
+ settings.each do |k, v|
+ settings[k] = settings[k].map{|v| v.to_i }.reject{|v| v==0} if k.start_with?('enable_vote')
+ end
+
+ @settings = Noosfero::Plugin::Settings.new(environment, VotePlugin, settings)
+ if request.post?
+ @settings.save!
+ session[:notice] = 'Settings succefully saved.'
+ redirect_to :action => 'index'
+ end
+ end
+
+end
diff --git a/plugins/vote/controllers/vote_plugin_profile_controller.rb b/plugins/vote/controllers/vote_plugin_profile_controller.rb
new file mode 100644
index 0000000..aa1e3a8
--- /dev/null
+++ b/plugins/vote/controllers/vote_plugin_profile_controller.rb
@@ -0,0 +1,57 @@
+class VotePluginProfileController < ProfileController
+
+ before_filter :login_required, :only => [:vote]
+
+ def vote
+ model = params[:model].to_sym
+ vote = params[:vote].to_i
+ settings = Noosfero::Plugin::Settings.new(environment, VotePlugin)
+ model_settings = settings.get_setting("enable_vote_#{model}")
+
+ unless model_settings && model_settings.include?(vote)
+ render_access_denied
+ return
+ end
+
+ object = target(model)
+ vote_target(object, vote)
+
+ render :update do |page|
+ model_settings.each do |v|
+ page.replace "vote_#{model}_#{params[:id]}_#{v}", instance_eval(&controller.vote_partial(object, v==1, false))
+ end
+ end
+ end
+
+ include VotePluginHelper
+
+ def reload_vote
+ model = params[:model].to_sym
+ vote = params[:vote].to_i
+ object = target(model)
+
+ render :update do |page|
+ page.replace "vote_#{model}_#{params[:id]}_#{vote}", instance_eval(&controller.vote_partial(object, vote==1, true))
+ end
+ end
+
+ protected
+
+ def target(model)
+ case model
+ when :article
+ profile.articles.find(params[:id])
+ when :comment
+ profile.comments_received.find(params[:id])
+ end
+ end
+
+ def vote_target(object, vote)
+ old_vote = user.votes.for_voteable(object).first
+ user.votes.for_voteable(object).each { |v| v.destroy }
+ if old_vote.nil? || old_vote.vote != vote
+ user.vote(object, vote)
+ end
+ end
+
+end
diff --git a/plugins/vote/lib/vote_plugin.rb b/plugins/vote/lib/vote_plugin.rb
new file mode 100644
index 0000000..139ca70
--- /dev/null
+++ b/plugins/vote/lib/vote_plugin.rb
@@ -0,0 +1,49 @@
+class VotePlugin < Noosfero::Plugin
+
+ def self.plugin_name
+ "Vote Plugin"
+ end
+
+ def self.plugin_description
+ _("Provide buttons to like/dislike a articles and comments.")
+ end
+
+ def stylesheet?
+ true
+ end
+
+ def js_files
+ 'vote_actions.js'
+ end
+
+ def self.enable_vote_article_default_setting
+ [-1, 1]
+ end
+
+ def self.enable_vote_comment_default_setting
+ [-1, 1]
+ end
+
+ def self.voters_limit_default_setting
+ 6
+ end
+
+ include VotePluginHelper
+
+ def comment_actions(comment)
+ like = vote_partial(comment)
+ dislike = vote_partial(comment, false)
+ lambda do
+ [{:link => instance_eval(&dislike), :action_bar => true}, {:link => instance_eval(&like), :action_bar => true}]
+ end
+ end
+
+ def article_header_extra_contents(article)
+ like = vote_partial(article)
+ dislike = vote_partial(article, false)
+ lambda do
+ content_tag('div', instance_eval(&dislike) + instance_eval(&like), :class => 'vote-actions')
+ end
+ end
+
+end
diff --git a/plugins/vote/lib/vote_plugin_helper.rb b/plugins/vote/lib/vote_plugin_helper.rb
new file mode 100644
index 0000000..dfa4ee4
--- /dev/null
+++ b/plugins/vote/lib/vote_plugin_helper.rb
@@ -0,0 +1,24 @@
+module VotePluginHelper
+
+ def vote_partial(target, like = true, load_voters=false)
+ vote = like ? 1 : -1
+ like_action = like ? 'like' : 'dislike'
+ type = target.kind_of?(Article) ? 'article' : target.kind_of?(Comment) ? 'comment' : nil
+
+ lambda do
+ settings = Noosfero::Plugin::Settings.new(environment, VotePlugin)
+
+ if settings.get_setting("enable_vote_#{type}").include?(vote)
+
+ voters = !load_voters ? nil : target.votes.where(:vote => vote).includes(:voter).limit(settings.get_setting('voters_limit')).map(&:voter)
+ active = user ? (like ? user.voted_for?(target) : user.voted_against?(target)) : false
+ count = like ? target.votes_for : target.votes_against
+
+ render(:partial => 'vote/vote.rhtml', :locals => {:target => target, :active => active, :action => like_action, :count => count, :voters => voters, :vote => vote, :model => type })
+ else
+ ""
+ end
+ end
+ end
+
+end
diff --git a/plugins/vote/public/style.css b/plugins/vote/public/style.css
new file mode 100644
index 0000000..4d6041a
--- /dev/null
+++ b/plugins/vote/public/style.css
@@ -0,0 +1,68 @@
+.vote-actions {
+ position: absolute;
+ top: 40px;
+ right: 0px;
+}
+
+.action {
+ float: right;
+ font-size: 8pt;
+}
+
+#article .action a {
+ text-decoration: none;
+}
+
+#article .action .like-action-active:hover {
+ color: #156E16;
+}
+
+#article .action .like-action-active {
+ color: #2A8C32;
+}
+
+#article .action .action-icon {
+ top: -1px;
+ left: -2px;
+ position: relative;
+ margin-left: 2px;
+}
+
+.action .dislike:before {
+ content: "\25bc";
+}
+
+.action .like:before {
+ content: "\25b2";
+}
+
+.dislike-action .like-action-counter {
+ border-left: 1px solid rgba(75,83,94,.3);
+ padding-left: 4px;
+}
+
+.action .vote-detail {
+ position: absolute;
+ width: 120px;
+ border: 1px solid #888a85;
+ background-color: #efefef;
+ padding-right: 2px;
+ height: auto;
+ display: block;
+ z-index: 12;
+}
+
+#article .action .vote-detail ul {
+ float: left;
+ padding: 5px;
+ margin: 0px;
+}
+
+#article .action .vote-detail li {
+ margin: 0 0 2px 0;
+ list-style-type: none;
+}
+
+#article .action .vote-detail img {
+ vertical-align: bottom;
+}
diff --git a/plugins/vote/public/vote_actions.js b/plugins/vote/public/vote_actions.js
new file mode 100644
index 0000000..86268af
--- /dev/null
+++ b/plugins/vote/public/vote_actions.js
@@ -0,0 +1,26 @@
+var openEvent = null;
+jQuery( document ).ready(function( $ ) {
+ $(".vote-action").live('mouseenter', function() {
+ var div = $(this);
+ if(openEvent==null)
+ openEvent = setInterval(function() { openVotersDialog(div); }, 500);
+ });
+ $(".vote-action").live('mouseleave', function() {
+ clearTimeout(openEvent);
+ openEvent = null;
+ });
+});
+
+function openVotersDialog(div) {
+ var $ = jQuery;
+ clearTimeout(openEvent);
+ var url = $(div).data('reload_url');
+ hideAllVoteDetail();
+ $.post(url);
+}
+
+jQuery('body').live('click', function() { hideAllVoteDetail(); });
+
+function hideAllVoteDetail() {
+ jQuery('.vote-detail').fadeOut('slow');
+}
diff --git a/plugins/vote/test/functional/vote_plugin_admin_controller_test.rb b/plugins/vote/test/functional/vote_plugin_admin_controller_test.rb
new file mode 100644
index 0000000..3412a21
--- /dev/null
+++ b/plugins/vote/test/functional/vote_plugin_admin_controller_test.rb
@@ -0,0 +1,30 @@
+require File.dirname(__FILE__) + '/../../../../test/test_helper'
+require File.dirname(__FILE__) + '/../../controllers/vote_plugin_admin_controller'
+
+# Re-raise errors caught by the controller.
+class VotePluginAdminController; def rescue_action(e) raise e end; end
+
+class VotePluginAdminControllerTest < ActionController::TestCase
+
+ def setup
+ @environment = Environment.default
+ @profile = create_user('profile').person
+ login_as(@profile.identifier)
+ end
+
+ attr_reader :environment
+
+ should 'save vote_plugin settings' do
+ post :index, :settings => {"enable_vote_article" => [1], "enable_vote_comment" => [-1]}
+ @settings = Noosfero::Plugin::Settings.new(environment.reload, VotePlugin)
+ assert_equal [1], @settings.settings[:enable_vote_article]
+ assert_equal [-1], @settings.settings[:enable_vote_comment]
+ assert_redirected_to :action => 'index'
+ end
+
+ should 'redirect to index after save' do
+ post :index, :settings => {"enable_vote_article" => [1]}
+ assert_redirected_to :action => 'index'
+ end
+
+end
diff --git a/plugins/vote/test/functional/vote_plugin_profile_controller_test.rb b/plugins/vote/test/functional/vote_plugin_profile_controller_test.rb
new file mode 100644
index 0000000..73db1e3
--- /dev/null
+++ b/plugins/vote/test/functional/vote_plugin_profile_controller_test.rb
@@ -0,0 +1,85 @@
+require File.dirname(__FILE__) + '/../../../../test/test_helper'
+require File.dirname(__FILE__) + '/../../controllers/vote_plugin_profile_controller'
+
+# Re-raise errors caught by the controller.
+class VotePluginProfileController; def rescue_action(e) raise e end; end
+
+class VotePluginProfileControllerTest < ActionController::TestCase
+
+ def setup
+ @profile = create_user('profile').person
+ @article = TinyMceArticle.create!(:profile => @profile, :name => 'An article')
+ @comment = Comment.new(:source => @article, :author => @profile, :body => 'test')
+ @comment.save!
+ login_as(@profile.identifier)
+ @environment = Environment.default
+ @environment.enable_plugin(VotePlugin)
+ self.stubs(:user).returns(@profile)
+ end
+
+ attr_reader :profile, :comment, :environment, :article
+
+ should 'do not vote if user is not logged in' do
+ logout
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1
+ assert_response 401
+ end
+
+ should 'not vote if value is not allowed' do
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 4
+ assert !profile.voted_on?(comment)
+ end
+
+ should 'not vote in a disallowed model' do
+ xhr :post, :vote, :profile => profile.identifier, :id => environment.id, :model => 'environment', :vote => 1
+ assert profile.votes.empty?
+ end
+
+ should 'like comment' do
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1
+ assert profile.voted_for?(comment)
+ end
+
+ should 'unlike comment' do
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1
+ assert !profile.voted_for?(comment)
+ end
+
+ should 'dislike comment' do
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1
+ assert profile.voted_against?(comment)
+ end
+
+ should 'undislike comment' do
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1
+ assert !profile.voted_against?(comment)
+ end
+
+ should 'dislike a liked comment' do
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1
+ assert profile.voted_against?(comment)
+ end
+
+ should 'like a disliked comment' do
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1
+ xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1
+ assert profile.voted_for?(comment)
+ end
+
+ should 'like article' do
+ xhr :post, :vote, :profile => profile.identifier, :id => article.id, :model => 'article', :vote => 1
+ assert profile.voted_for?(article)
+ end
+
+ should 'update views with new vote state' do
+ xhr :post, :vote, :profile => profile.identifier, :id => article.id, :model => 'article', :vote => 1
+ assert_select_rjs :replace do
+ assert_select "#vote_article_#{article.id}_1"
+ assert_select "#vote_article_#{article.id}_-1"
+ end
+ end
+
+end
diff --git a/plugins/vote/test/test_helper.rb b/plugins/vote/test/test_helper.rb
new file mode 100644
index 0000000..cca1fd3
--- /dev/null
+++ b/plugins/vote/test/test_helper.rb
@@ -0,0 +1 @@
+require File.dirname(__FILE__) + '/../../../test/test_helper'
diff --git a/plugins/vote/test/unit/vote_plugin_test.rb b/plugins/vote/test/unit/vote_plugin_test.rb
new file mode 100644
index 0000000..951f87a
--- /dev/null
+++ b/plugins/vote/test/unit/vote_plugin_test.rb
@@ -0,0 +1,30 @@
+require File.dirname(__FILE__) + '/../../../../test/test_helper'
+
+class VotePluginTest < ActiveSupport::TestCase
+
+ def setup
+ @plugin = VotePlugin.new
+ @person = create_user('user').person
+ @article = TinyMceArticle.create!(:profile => @person, :name => 'An article')
+ @comment = Comment.create!(:source => @article, :author => @person, :body => 'test')
+ end
+
+ attr_reader :plugin, :comment, :article
+
+ should 'have a stylesheet' do
+ assert plugin.stylesheet?
+ end
+
+ should 'have a javascript' do
+ assert plugin.js_files
+ end
+
+ should 'return proc to display partials to vote for comments' do
+ assert plugin.comment_actions(comment).kind_of?(Proc)
+ end
+
+ should 'return proc to display partials to vote for articles' do
+ assert plugin.article_header_extra_contents(article).kind_of?(Proc)
+ end
+
+end
diff --git a/plugins/vote/views/vote/_vote.rhtml b/plugins/vote/views/vote/_vote.rhtml
new file mode 100644
index 0000000..57fce2d
--- /dev/null
+++ b/plugins/vote/views/vote/_vote.rhtml
@@ -0,0 +1,22 @@
+<%
+url = url_for(:controller => 'vote_plugin_profile', :profile => profile.identifier, :action => :vote, :id => target.id, :model => model, :vote => vote)
+reload_url = url_for(:controller => 'vote_plugin_profile', :profile => profile.identifier, :action => :reload_vote, :id => target.id, :model => model, :vote => vote)
+%>
+
+ class="vote-action action <%= action %>-action">
+
+ <%= link_to_remote content_tag(:span, count, :class=>'like-action-counter') + content_tag(:span, '', :class=>"action-icon #{action}"), :url => url, :html => {:class => "#{active ? 'like-action-active':''} #{user ? '':'disabled'}"} %>
+
+ <% if !voters.blank? %>
+
+
+ <% voters.each do |voter| %>
+
+
+ <% end %>
+
diff --git a/plugins/vote/views/vote_plugin_admin/index.rhtml b/plugins/vote/views/vote_plugin_admin/index.rhtml
new file mode 100644
index 0000000..2cbecd6
--- /dev/null
+++ b/plugins/vote/views/vote_plugin_admin/index.rhtml
@@ -0,0 +1,21 @@
+