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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
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 @@ |
1 | +require File.dirname(__FILE__) + '/../../../test/test_helper' | ... | ... |
... | ... | @@ -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 @@ |
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 @@ |
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 | + | ... | ... |