Commit 772c27b56f93207382bc7dd577708a5ac9181de1
1 parent
db23d209
Exists in
master
and in
29 other branches
Add a plugin to vote on articles and comments
Showing
12 changed files
with
431 additions
and
0 deletions
Show diff stats
plugins/vote/controllers/vote_plugin_admin_controller.rb
0 → 100644
@@ -0,0 +1,18 @@ | @@ -0,0 +1,18 @@ | ||
1 | +class VotePluginAdminController < AdminController | ||
2 | + | ||
3 | + def index | ||
4 | + settings = params[:settings] | ||
5 | + settings ||= {} | ||
6 | + settings.each do |k, v| | ||
7 | + settings[k] = settings[k].map{|v| v.to_i }.reject{|v| v==0} if k.start_with?('enable_vote') | ||
8 | + end | ||
9 | + | ||
10 | + @settings = Noosfero::Plugin::Settings.new(environment, VotePlugin, settings) | ||
11 | + if request.post? | ||
12 | + @settings.save! | ||
13 | + session[:notice] = 'Settings succefully saved.' | ||
14 | + redirect_to :action => 'index' | ||
15 | + end | ||
16 | + end | ||
17 | + | ||
18 | +end |
plugins/vote/controllers/vote_plugin_profile_controller.rb
0 → 100644
@@ -0,0 +1,57 @@ | @@ -0,0 +1,57 @@ | ||
1 | +class VotePluginProfileController < ProfileController | ||
2 | + | ||
3 | + before_filter :login_required, :only => [:vote] | ||
4 | + | ||
5 | + def vote | ||
6 | + model = params[:model].to_sym | ||
7 | + vote = params[:vote].to_i | ||
8 | + settings = Noosfero::Plugin::Settings.new(environment, VotePlugin) | ||
9 | + model_settings = settings.get_setting("enable_vote_#{model}") | ||
10 | + | ||
11 | + unless model_settings && model_settings.include?(vote) | ||
12 | + render_access_denied | ||
13 | + return | ||
14 | + end | ||
15 | + | ||
16 | + object = target(model) | ||
17 | + vote_target(object, vote) | ||
18 | + | ||
19 | + render :update do |page| | ||
20 | + model_settings.each do |v| | ||
21 | + page.replace "vote_#{model}_#{params[:id]}_#{v}", instance_eval(&controller.vote_partial(object, v==1, false)) | ||
22 | + end | ||
23 | + end | ||
24 | + end | ||
25 | + | ||
26 | + include VotePluginHelper | ||
27 | + | ||
28 | + def reload_vote | ||
29 | + model = params[:model].to_sym | ||
30 | + vote = params[:vote].to_i | ||
31 | + object = target(model) | ||
32 | + | ||
33 | + render :update do |page| | ||
34 | + page.replace "vote_#{model}_#{params[:id]}_#{vote}", instance_eval(&controller.vote_partial(object, vote==1, true)) | ||
35 | + end | ||
36 | + end | ||
37 | + | ||
38 | + protected | ||
39 | + | ||
40 | + def target(model) | ||
41 | + case model | ||
42 | + when :article | ||
43 | + profile.articles.find(params[:id]) | ||
44 | + when :comment | ||
45 | + profile.comments_received.find(params[:id]) | ||
46 | + end | ||
47 | + end | ||
48 | + | ||
49 | + def vote_target(object, vote) | ||
50 | + old_vote = user.votes.for_voteable(object).first | ||
51 | + user.votes.for_voteable(object).each { |v| v.destroy } | ||
52 | + if old_vote.nil? || old_vote.vote != vote | ||
53 | + user.vote(object, vote) | ||
54 | + end | ||
55 | + end | ||
56 | + | ||
57 | +end |
@@ -0,0 +1,49 @@ | @@ -0,0 +1,49 @@ | ||
1 | +class VotePlugin < Noosfero::Plugin | ||
2 | + | ||
3 | + def self.plugin_name | ||
4 | + "Vote Plugin" | ||
5 | + end | ||
6 | + | ||
7 | + def self.plugin_description | ||
8 | + _("Provide buttons to like/dislike a articles and comments.") | ||
9 | + end | ||
10 | + | ||
11 | + def stylesheet? | ||
12 | + true | ||
13 | + end | ||
14 | + | ||
15 | + def js_files | ||
16 | + 'vote_actions.js' | ||
17 | + end | ||
18 | + | ||
19 | + def self.enable_vote_article_default_setting | ||
20 | + [-1, 1] | ||
21 | + end | ||
22 | + | ||
23 | + def self.enable_vote_comment_default_setting | ||
24 | + [-1, 1] | ||
25 | + end | ||
26 | + | ||
27 | + def self.voters_limit_default_setting | ||
28 | + 6 | ||
29 | + end | ||
30 | + | ||
31 | + include VotePluginHelper | ||
32 | + | ||
33 | + def comment_actions(comment) | ||
34 | + like = vote_partial(comment) | ||
35 | + dislike = vote_partial(comment, false) | ||
36 | + lambda do | ||
37 | + [{:link => instance_eval(&dislike), :action_bar => true}, {:link => instance_eval(&like), :action_bar => true}] | ||
38 | + end | ||
39 | + end | ||
40 | + | ||
41 | + def article_header_extra_contents(article) | ||
42 | + like = vote_partial(article) | ||
43 | + dislike = vote_partial(article, false) | ||
44 | + lambda do | ||
45 | + content_tag('div', instance_eval(&dislike) + instance_eval(&like), :class => 'vote-actions') | ||
46 | + end | ||
47 | + end | ||
48 | + | ||
49 | +end |
@@ -0,0 +1,24 @@ | @@ -0,0 +1,24 @@ | ||
1 | +module VotePluginHelper | ||
2 | + | ||
3 | + def vote_partial(target, like = true, load_voters=false) | ||
4 | + vote = like ? 1 : -1 | ||
5 | + like_action = like ? 'like' : 'dislike' | ||
6 | + type = target.kind_of?(Article) ? 'article' : target.kind_of?(Comment) ? 'comment' : nil | ||
7 | + | ||
8 | + lambda do | ||
9 | + settings = Noosfero::Plugin::Settings.new(environment, VotePlugin) | ||
10 | + | ||
11 | + if settings.get_setting("enable_vote_#{type}").include?(vote) | ||
12 | + | ||
13 | + voters = !load_voters ? nil : target.votes.where(:vote => vote).includes(:voter).limit(settings.get_setting('voters_limit')).map(&:voter) | ||
14 | + active = user ? (like ? user.voted_for?(target) : user.voted_against?(target)) : false | ||
15 | + count = like ? target.votes_for : target.votes_against | ||
16 | + | ||
17 | + render(:partial => 'vote/vote.rhtml', :locals => {:target => target, :active => active, :action => like_action, :count => count, :voters => voters, :vote => vote, :model => type }) | ||
18 | + else | ||
19 | + "" | ||
20 | + end | ||
21 | + end | ||
22 | + end | ||
23 | + | ||
24 | +end |
@@ -0,0 +1,68 @@ | @@ -0,0 +1,68 @@ | ||
1 | +.vote-actions { | ||
2 | + position: absolute; | ||
3 | + top: 40px; | ||
4 | + right: 0px; | ||
5 | +} | ||
6 | + | ||
7 | +.action { | ||
8 | + float: right; | ||
9 | + font-size: 8pt; | ||
10 | +} | ||
11 | + | ||
12 | +#article .action a { | ||
13 | + text-decoration: none; | ||
14 | +} | ||
15 | + | ||
16 | +#article .action .like-action-active:hover { | ||
17 | + color: #156E16; | ||
18 | +} | ||
19 | + | ||
20 | +#article .action .like-action-active { | ||
21 | + color: #2A8C32; | ||
22 | +} | ||
23 | + | ||
24 | +#article .action .action-icon { | ||
25 | + top: -1px; | ||
26 | + left: -2px; | ||
27 | + position: relative; | ||
28 | + margin-left: 2px; | ||
29 | +} | ||
30 | + | ||
31 | +.action .dislike:before { | ||
32 | + content: "\25bc"; | ||
33 | +} | ||
34 | + | ||
35 | +.action .like:before { | ||
36 | + content: "\25b2"; | ||
37 | +} | ||
38 | + | ||
39 | +.dislike-action .like-action-counter { | ||
40 | + border-left: 1px solid rgba(75,83,94,.3); | ||
41 | + padding-left: 4px; | ||
42 | +} | ||
43 | + | ||
44 | +.action .vote-detail { | ||
45 | + position: absolute; | ||
46 | + width: 120px; | ||
47 | + border: 1px solid #888a85; | ||
48 | + background-color: #efefef; | ||
49 | + padding-right: 2px; | ||
50 | + height: auto; | ||
51 | + display: block; | ||
52 | + z-index: 12; | ||
53 | +} | ||
54 | + | ||
55 | +#article .action .vote-detail ul { | ||
56 | + float: left; | ||
57 | + padding: 5px; | ||
58 | + margin: 0px; | ||
59 | +} | ||
60 | + | ||
61 | +#article .action .vote-detail li { | ||
62 | + margin: 0 0 2px 0; | ||
63 | + list-style-type: none; | ||
64 | +} | ||
65 | + | ||
66 | +#article .action .vote-detail img { | ||
67 | + vertical-align: bottom; | ||
68 | +} |
@@ -0,0 +1,26 @@ | @@ -0,0 +1,26 @@ | ||
1 | +var openEvent = null; | ||
2 | +jQuery( document ).ready(function( $ ) { | ||
3 | + $(".vote-action").live('mouseenter', function() { | ||
4 | + var div = $(this); | ||
5 | + if(openEvent==null) | ||
6 | + openEvent = setInterval(function() { openVotersDialog(div); }, 500); | ||
7 | + }); | ||
8 | + $(".vote-action").live('mouseleave', function() { | ||
9 | + clearTimeout(openEvent); | ||
10 | + openEvent = null; | ||
11 | + }); | ||
12 | +}); | ||
13 | + | ||
14 | +function openVotersDialog(div) { | ||
15 | + var $ = jQuery; | ||
16 | + clearTimeout(openEvent); | ||
17 | + var url = $(div).data('reload_url'); | ||
18 | + hideAllVoteDetail(); | ||
19 | + $.post(url); | ||
20 | +} | ||
21 | + | ||
22 | +jQuery('body').live('click', function() { hideAllVoteDetail(); }); | ||
23 | + | ||
24 | +function hideAllVoteDetail() { | ||
25 | + jQuery('.vote-detail').fadeOut('slow'); | ||
26 | +} |
plugins/vote/test/functional/vote_plugin_admin_controller_test.rb
0 → 100644
@@ -0,0 +1,30 @@ | @@ -0,0 +1,30 @@ | ||
1 | +require File.dirname(__FILE__) + '/../../../../test/test_helper' | ||
2 | +require File.dirname(__FILE__) + '/../../controllers/vote_plugin_admin_controller' | ||
3 | + | ||
4 | +# Re-raise errors caught by the controller. | ||
5 | +class VotePluginAdminController; def rescue_action(e) raise e end; end | ||
6 | + | ||
7 | +class VotePluginAdminControllerTest < ActionController::TestCase | ||
8 | + | ||
9 | + def setup | ||
10 | + @environment = Environment.default | ||
11 | + @profile = create_user('profile').person | ||
12 | + login_as(@profile.identifier) | ||
13 | + end | ||
14 | + | ||
15 | + attr_reader :environment | ||
16 | + | ||
17 | + should 'save vote_plugin settings' do | ||
18 | + post :index, :settings => {"enable_vote_article" => [1], "enable_vote_comment" => [-1]} | ||
19 | + @settings = Noosfero::Plugin::Settings.new(environment.reload, VotePlugin) | ||
20 | + assert_equal [1], @settings.settings[:enable_vote_article] | ||
21 | + assert_equal [-1], @settings.settings[:enable_vote_comment] | ||
22 | + assert_redirected_to :action => 'index' | ||
23 | + end | ||
24 | + | ||
25 | + should 'redirect to index after save' do | ||
26 | + post :index, :settings => {"enable_vote_article" => [1]} | ||
27 | + assert_redirected_to :action => 'index' | ||
28 | + end | ||
29 | + | ||
30 | +end |
plugins/vote/test/functional/vote_plugin_profile_controller_test.rb
0 → 100644
@@ -0,0 +1,85 @@ | @@ -0,0 +1,85 @@ | ||
1 | +require File.dirname(__FILE__) + '/../../../../test/test_helper' | ||
2 | +require File.dirname(__FILE__) + '/../../controllers/vote_plugin_profile_controller' | ||
3 | + | ||
4 | +# Re-raise errors caught by the controller. | ||
5 | +class VotePluginProfileController; def rescue_action(e) raise e end; end | ||
6 | + | ||
7 | +class VotePluginProfileControllerTest < ActionController::TestCase | ||
8 | + | ||
9 | + def setup | ||
10 | + @profile = create_user('profile').person | ||
11 | + @article = TinyMceArticle.create!(:profile => @profile, :name => 'An article') | ||
12 | + @comment = Comment.new(:source => @article, :author => @profile, :body => 'test') | ||
13 | + @comment.save! | ||
14 | + login_as(@profile.identifier) | ||
15 | + @environment = Environment.default | ||
16 | + @environment.enable_plugin(VotePlugin) | ||
17 | + self.stubs(:user).returns(@profile) | ||
18 | + end | ||
19 | + | ||
20 | + attr_reader :profile, :comment, :environment, :article | ||
21 | + | ||
22 | + should 'do not vote if user is not logged in' do | ||
23 | + logout | ||
24 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1 | ||
25 | + assert_response 401 | ||
26 | + end | ||
27 | + | ||
28 | + should 'not vote if value is not allowed' do | ||
29 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 4 | ||
30 | + assert !profile.voted_on?(comment) | ||
31 | + end | ||
32 | + | ||
33 | + should 'not vote in a disallowed model' do | ||
34 | + xhr :post, :vote, :profile => profile.identifier, :id => environment.id, :model => 'environment', :vote => 1 | ||
35 | + assert profile.votes.empty? | ||
36 | + end | ||
37 | + | ||
38 | + should 'like comment' do | ||
39 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1 | ||
40 | + assert profile.voted_for?(comment) | ||
41 | + end | ||
42 | + | ||
43 | + should 'unlike comment' do | ||
44 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1 | ||
45 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1 | ||
46 | + assert !profile.voted_for?(comment) | ||
47 | + end | ||
48 | + | ||
49 | + should 'dislike comment' do | ||
50 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1 | ||
51 | + assert profile.voted_against?(comment) | ||
52 | + end | ||
53 | + | ||
54 | + should 'undislike comment' do | ||
55 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1 | ||
56 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1 | ||
57 | + assert !profile.voted_against?(comment) | ||
58 | + end | ||
59 | + | ||
60 | + should 'dislike a liked comment' do | ||
61 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1 | ||
62 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1 | ||
63 | + assert profile.voted_against?(comment) | ||
64 | + end | ||
65 | + | ||
66 | + should 'like a disliked comment' do | ||
67 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => -1 | ||
68 | + xhr :post, :vote, :profile => profile.identifier, :id => comment.id, :model => 'comment', :vote => 1 | ||
69 | + assert profile.voted_for?(comment) | ||
70 | + end | ||
71 | + | ||
72 | + should 'like article' do | ||
73 | + xhr :post, :vote, :profile => profile.identifier, :id => article.id, :model => 'article', :vote => 1 | ||
74 | + assert profile.voted_for?(article) | ||
75 | + end | ||
76 | + | ||
77 | + should 'update views with new vote state' do | ||
78 | + xhr :post, :vote, :profile => profile.identifier, :id => article.id, :model => 'article', :vote => 1 | ||
79 | + assert_select_rjs :replace do | ||
80 | + assert_select "#vote_article_#{article.id}_1" | ||
81 | + assert_select "#vote_article_#{article.id}_-1" | ||
82 | + end | ||
83 | + end | ||
84 | + | ||
85 | +end |
@@ -0,0 +1 @@ | @@ -0,0 +1 @@ | ||
1 | +require File.dirname(__FILE__) + '/../../../test/test_helper' |
@@ -0,0 +1,30 @@ | @@ -0,0 +1,30 @@ | ||
1 | +require File.dirname(__FILE__) + '/../../../../test/test_helper' | ||
2 | + | ||
3 | +class VotePluginTest < ActiveSupport::TestCase | ||
4 | + | ||
5 | + def setup | ||
6 | + @plugin = VotePlugin.new | ||
7 | + @person = create_user('user').person | ||
8 | + @article = TinyMceArticle.create!(:profile => @person, :name => 'An article') | ||
9 | + @comment = Comment.create!(:source => @article, :author => @person, :body => 'test') | ||
10 | + end | ||
11 | + | ||
12 | + attr_reader :plugin, :comment, :article | ||
13 | + | ||
14 | + should 'have a stylesheet' do | ||
15 | + assert plugin.stylesheet? | ||
16 | + end | ||
17 | + | ||
18 | + should 'have a javascript' do | ||
19 | + assert plugin.js_files | ||
20 | + end | ||
21 | + | ||
22 | + should 'return proc to display partials to vote for comments' do | ||
23 | + assert plugin.comment_actions(comment).kind_of?(Proc) | ||
24 | + end | ||
25 | + | ||
26 | + should 'return proc to display partials to vote for articles' do | ||
27 | + assert plugin.article_header_extra_contents(article).kind_of?(Proc) | ||
28 | + end | ||
29 | + | ||
30 | +end |
@@ -0,0 +1,22 @@ | @@ -0,0 +1,22 @@ | ||
1 | +<% | ||
2 | +url = url_for(:controller => 'vote_plugin_profile', :profile => profile.identifier, :action => :vote, :id => target.id, :model => model, :vote => vote) | ||
3 | +reload_url = url_for(:controller => 'vote_plugin_profile', :profile => profile.identifier, :action => :reload_vote, :id => target.id, :model => model, :vote => vote) | ||
4 | +%> | ||
5 | + | ||
6 | +<span id="vote_<%= model %>_<%= target.id %>_<%= vote %>" data-reload_url=<%= reload_url %> class="vote-action action <%= action %>-action"> | ||
7 | + | ||
8 | + <%= 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'}"} %> | ||
9 | + | ||
10 | + <% if !voters.blank? %> | ||
11 | + <span class="vote-detail"> | ||
12 | + <ul> | ||
13 | + <% voters.each do |voter| %> | ||
14 | + <li> | ||
15 | + <%= link_to image_tag(profile_icon(voter, :icon)) + content_tag('span', voter.short_name), | ||
16 | + voter.url, :title => voter.short_name %> | ||
17 | + </li> | ||
18 | + <% end %> | ||
19 | + </ul> | ||
20 | + </span> | ||
21 | + <% end %> | ||
22 | +</span> |
@@ -0,0 +1,21 @@ | @@ -0,0 +1,21 @@ | ||
1 | +<h1><%= _('Vote settings')%></h1> | ||
2 | + | ||
3 | +<% form_for(:settings) do |f| %> | ||
4 | + | ||
5 | + <% ['article', 'comment'].each do |model| %> | ||
6 | + <h5><%= _('Enable on %s:' % model) %></h5> | ||
7 | + <%= f.check_box("enable_vote_#{model}", {:multiple => true}, 1) + _('Like') %> | ||
8 | + <%= f.check_box("enable_vote_#{model}", {:multiple => true}, -1) + _('Dislike') %> | ||
9 | + <% end %> | ||
10 | + <br/><br/> | ||
11 | + | ||
12 | + <strong> | ||
13 | + <%= labelled_form_field _('Limit of voters to display:'), f.text_field(:voters_limit, :size => 3) %> | ||
14 | + </strong> | ||
15 | + | ||
16 | + <% button_bar do %> | ||
17 | + <%= submit_button(:save, _('Save'), :cancel => {:controller => 'plugins', :action => 'index'}) %> | ||
18 | + <% end %> | ||
19 | + | ||
20 | +<% end %> | ||
21 | + |