├── app ├── helpers │ └── vote_helper.rb ├── views │ └── vote │ │ ├── _view_layouts_base_content.html.erb │ │ └── result.html.erb ├── models │ └── votes.rb └── controllers │ └── vote_controller.rb ├── config ├── locales │ ├── ko.yml │ └── en.yml └── routes.rb ├── screenshot └── screenshot.png ├── test ├── test_helper.rb ├── unit │ └── votes_test.rb └── functional │ └── vote_controller_test.rb ├── db └── migrate │ └── 001_create_votes.rb ├── .gitignore ├── init.rb ├── assets ├── stylesheets │ └── vote.css └── javascripts │ └── vote.js ├── lib └── vote_application_hooks.rb ├── LICENSE └── README.rdoc /app/helpers/vote_helper.rb: -------------------------------------------------------------------------------- 1 | module VoteHelper 2 | end 3 | -------------------------------------------------------------------------------- /config/locales/ko.yml: -------------------------------------------------------------------------------- 1 | ko: 2 | label_vote_result: "투표 순위" 3 | label_vote_count: "득표" -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | en: 2 | label_vote_result: "Vote Result" 3 | label_vote_count: "Votes" -------------------------------------------------------------------------------- /screenshot/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jongha/redmine_vote/HEAD/screenshot/screenshot.png -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 1 | # Load the Redmine helper 2 | require File.expand_path(File.dirname(__FILE__) + '/../../../test/test_helper') 3 | -------------------------------------------------------------------------------- /test/unit/votes_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class VotesTest < ActiveSupport::TestCase 4 | 5 | # Replace this with your real tests. 6 | def test_truth 7 | assert true 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /db/migrate/001_create_votes.rb: -------------------------------------------------------------------------------- 1 | class CreateVotes < ActiveRecord::Migration 2 | def change 3 | create_table :votes do |t| 4 | t.integer :message_id 5 | t.integer :user_id 6 | t.integer :point 7 | end 8 | end 9 | end 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.gem 2 | *.rbc 3 | .bundle 4 | .config 5 | coverage 6 | InstalledFiles 7 | lib/bundler/man 8 | pkg 9 | rdoc 10 | spec/reports 11 | test/tmp 12 | test/version_tmp 13 | tmp 14 | 15 | # YARD artifacts 16 | .yardoc 17 | _yardoc 18 | doc/ 19 | -------------------------------------------------------------------------------- /test/functional/vote_controller_test.rb: -------------------------------------------------------------------------------- 1 | require File.expand_path('../../test_helper', __FILE__) 2 | 3 | class VoteControllerTest < ActionController::TestCase 4 | # Replace this with your real tests. 5 | def test_truth 6 | assert true 7 | end 8 | end 9 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # Plugin's routes 2 | # See: http://guides.rubyonrails.org/routing.html 3 | 4 | Rails.application.routes.draw do 5 | get 'boards/:board_id/topics/:message_id/vote', :to => 'vote#get' 6 | post 'boards/:board_id/topics/:message_id/vote', :to => 'vote#add' 7 | get 'boards/:board_id/vote/result', :to => 'vote#result' 8 | end 9 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | require 'redmine' 2 | require 'vote_application_hooks' 3 | 4 | Redmine::Plugin.register :redmine_vote do 5 | name 'Redmine Vote plugin' 6 | author 'Jong-Ha Ahn' 7 | description 'This is a plugin for Redmine' 8 | version '1.2.2' 9 | url 'http://github.com/jongha/redmine_vote' 10 | author_url 'http://www.mrlatte.net' 11 | end 12 | -------------------------------------------------------------------------------- /assets/stylesheets/vote.css: -------------------------------------------------------------------------------- 1 | .vote-area { 2 | display:none; 3 | padding: 5px 17px; 4 | margin: 10px 10px 30px 0px; 5 | border: 1px solid #CCCCCC; 6 | text-align:center; 7 | font-size: 14px; 8 | font-weight: bold; 9 | background-color: #EEEEEE; 10 | float: left; 11 | } 12 | 13 | .vote-result { 14 | margin: 0px 0px 15px 0px; 15 | } -------------------------------------------------------------------------------- /lib/vote_application_hooks.rb: -------------------------------------------------------------------------------- 1 | class VoteHeaderHooks < Redmine::Hook::ViewListener 2 | 3 | def view_layouts_base_html_head(context = {}) 4 | o = stylesheet_link_tag('vote', :plugin => 'redmine_vote') 5 | o << javascript_include_tag('vote', :plugin => 'redmine_vote') 6 | return o 7 | end 8 | end 9 | 10 | class VoteLayoutBaseContentHooks < Redmine::Hook::ViewListener 11 | render_on :view_layouts_base_content, :partial => 'vote/view_layouts_base_content' 12 | end -------------------------------------------------------------------------------- /app/views/vote/_view_layouts_base_content.html.erb: -------------------------------------------------------------------------------- 1 | <% unless User.current.login == '' %> 2 | <% unless @board.nil? || @board[:id].nil? || @topic.nil? || @topic.id.nil? %> 3 |
8 | 9 | <% end %> 10 | 11 | <% end %> 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Jong-Ha Ahn 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of 6 | this software and associated documentation files (the "Software"), to deal in 7 | the Software without restriction, including without limitation the rights to 8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of 9 | the Software, and to permit persons to whom the Software is furnished to do so, 10 | subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS 17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR 18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER 19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /app/models/votes.rb: -------------------------------------------------------------------------------- 1 | class Votes < ActiveRecord::Base 2 | unloadable 3 | 4 | def add_vote(message_id, user_id, point = "0") 5 | votes = Votes.where('message_id = %d and user_id = %d' % [message_id, user_id]).first 6 | 7 | point = point.to_i 8 | if votes 9 | votes.point = (votes.point == point)? 0 : point 10 | votes.save! 11 | else 12 | votes = Votes.new 13 | votes.message_id = message_id 14 | votes.user_id = user_id 15 | votes.point = point 16 | votes.save! 17 | end 18 | 19 | return get_point(message_id) 20 | end 21 | 22 | def get_point(message_id) 23 | return Votes.where('message_id = %d' % message_id).sum(:point) 24 | end 25 | 26 | def get_points(user_id, message_id) 27 | return result = { 28 | "plus" => Votes.where('message_id = %d and point > 0' % message_id).sum(:point), 29 | "minus" => Votes.where('message_id = %d and point < 0' % message_id).sum(:point), 30 | "zero" => Votes.where('message_id = %d and point = 0' % message_id).sum(:point), 31 | "vote" => Votes.where('message_id = %d and user_id = %d' % [message_id, user_id]).sum(:point), 32 | } 33 | end 34 | end 35 | -------------------------------------------------------------------------------- /app/views/vote/result.html.erb: -------------------------------------------------------------------------------- 1 | <% if @message.length > 0 %> 2 |
}[https://gemnasium.com/jongha/redmine_vote]
3 |
4 | This is redmine vote plugin. Its style is similar to stackoverflow. You can vote for each message with a positive or negative point. When you install this plugin votes table is created internally. This plugin shows the sum of points the message using internal table. If you want to know the reaction of message in Redmine forum, this plugin helps you. And also if you want more functions of the plugin, you can add the issue on github freely. Thanks.
5 |
6 | == Screenshot
7 |
8 | {
}[https://raw.github.com/jongha/redmine_vote/master/screenshot/screenshot.png]
9 |
10 | == Installing a plugin
11 |
12 | 1. For Redmine 1.x: rake db:migrate_plugins RAILS_ENV=production
13 |
14 | 2. For Redmine 2.x: rake redmine:plugins:migrate RAILS_ENV=production
15 |
16 | 3. (Re)start Redmine.
17 |
18 | == Uninstalling a plugin
19 |
20 | 1. For Redmine 1.x: rake db:migrate:plugin NAME=redmine_vote VERSION=0.0.1 RAILS_ENV=production
21 |
22 | 2. For Redmine 2.x: rake redmine:plugins:migrate NAME=redmine_vote VERSION=0.0.1 RAILS_ENV=production
23 |
24 | 3. Remove your plugin from the plugins folder: #{RAILS_ROOT}/plugins (Redmine 2.x) or #{RAILS_ROOT}/vendor/plugins (Redmine 1.x)..
25 |
26 | 4. (Re)start Redmine.
27 |
28 | == License
29 |
30 | redmine_vote is available under the terms of the MIT License.
31 |
--------------------------------------------------------------------------------
/app/controllers/vote_controller.rb:
--------------------------------------------------------------------------------
1 | class VoteController < ApplicationController
2 | unloadable
3 |
4 | before_filter :require_login
5 | before_filter :find_user #, :authorize
6 | before_filter :init_votes
7 |
8 | MAX_VOTELIST = 5
9 |
10 | def add
11 | find_board_and_topic
12 |
13 | if ['-1', '1', '0'].include? params[:point] then
14 | @point = @votes.add_vote(@message.id, @user.id, params[:point])
15 | end
16 |
17 | get
18 | end
19 |
20 | def get
21 | find_board_and_topic
22 |
23 | # @point = @votes.get_point(@message.id)
24 | result = @votes.get_points(@user.id, @message.id)
25 | result['point'] = result['plus'] + result['minus']
26 | render :json => result
27 |
28 | end
29 |
30 | def result
31 | @board = Board.find(params[:board_id])
32 | @votes = Votes\
33 | .select('message_id, sum(point) as sump')\
34 | .joins('right join messages on messages.board_id = %s and votes.message_id = messages.id and messages.parent_id is null' % @board.id)\
35 | .where('messages.locked' => 0)\
36 | .group('message_id')\
37 | .order('sum(point) desc, messages.replies_count desc, messages.id desc')\
38 | .limit(MAX_VOTELIST)
39 |
40 | messages = Array.new
41 | @votes_message = {}
42 | @votes.each do |v|
43 | messages.push(v.message_id)
44 | @votes_message[v.message_id] = v.sump
45 | end
46 |
47 | @message = @board.messages\
48 | .joins('inner join (select message_id, sum(point) as sump from votes group by message_id) as votes on messages.id = votes.message_id')\
49 | .where('messages.id' => messages)\
50 | .reorder('votes.sump desc, messages.replies_count desc, messages.id desc')
51 | end
52 |
53 | private
54 | def init_votes
55 | @votes = Votes.new
56 | end
57 |
58 | def find_user
59 | @user = User.current
60 | end
61 |
62 | def find_board_and_topic
63 | begin
64 | @board = Board.find(params[:board_id])
65 | @message = @board.messages.find(params[:message_id])
66 |
67 | rescue ActiveRecord::RecordNotFound
68 | render_404
69 | end
70 | end
71 | end
--------------------------------------------------------------------------------
/assets/javascripts/vote.js:
--------------------------------------------------------------------------------
1 | $(document).ready(function() {
2 | var baseObj = $("#vote-base-url");
3 | var base = "";
4 |
5 | if(baseObj.length > 0) {
6 | base = baseObj.val();
7 | }
8 |
9 | if($(".controller-messages").length && $("#vote").length) {
10 | var clr = $("").css({ clear: "right" });
11 | var queue = [];
12 | $(".message").each(
13 | function() {
14 | queue.push($(this));
15 | }
16 | );
17 |
18 | var execQueue = function() {
19 | if(queue.length) {
20 | queueStep(queue.shift());
21 | }
22 | };
23 |
24 | var queueStep = function(that) {
25 | var deferred = $.Deferred();
26 | var messageId = that.attr("id");
27 | var vote = $("#vote").clone().show().attr({ id: null });
28 | if(messageId) {
29 | vote.data({ topic: parseInt(String(messageId).replace(/message-/gi, "")) });
30 | }
31 | that.css({ "clear": "both" }).prepend(vote);
32 |
33 | var board = vote.data("board");
34 | var topic = vote.data("topic");
35 | var votePoint = vote.find(".vote-point:first");
36 | var voteCheck = vote.find(".vote-check:first");
37 |
38 | $.ajax({
39 | type: "GET",
40 | url: base + "boards/" + board + "/topics/" + topic + "/vote",
41 | cache: false,
42 | error: function(jqXHR, textStatus, errorThrown) {
43 | votePoint.html("-");
44 | },
45 | success: function(data, textStatus, jqXHR) {
46 | votePoint.html(data.point);
47 | voteCheck.html(data.vote ? "☑" : "✅");
48 | }
49 |
50 | }).always(function() {
51 | deferred.always();
52 |
53 | vote.find(".vote-button").bind("click", function(event) {
54 | event.preventDefault();
55 | var point = $(this).data("point");
56 | $.ajax({
57 | type: "POST",
58 | url: base + "boards/" + board + "/topics/" + topic + "/vote",
59 | data: { point: point },
60 | cache: false,
61 | success: function(data, textStatus, jqXHR) {
62 | votePoint.html(data.point);
63 | voteCheck.html(data.vote ? "☑" : "✅");
64 | }
65 | });
66 | });
67 |
68 | execQueue();
69 | });
70 | return deferred.promise();
71 | };
72 | execQueue();
73 | };
74 |
75 | var re = /https?:\/\/.*\/projects\/.*\/boards\/([0-9]*)/;
76 | var url = document.URL;
77 | var match = url.match(re);
78 | var result = "#vote-result";
79 |
80 | if(match) {
81 | $("")
82 | .attr("id", "vote-result-box")
83 | .insertAfter($("#content").find("p.breadcrumb"));
84 |
85 | $.ajax({
86 | type: "GET",
87 | url: base + "boards/" + match[1] + "/vote/result",
88 | cache: false,
89 | success: function(data, textStatus, jqXHR) {
90 | if($(result).length === 0) {
91 | var html = $(data).find(result).html();
92 | if(html) {
93 | $("#vote-result-box").addClass("vote-result").html(html);
94 | }else {
95 | $("#vote-result-box").remove();
96 | }
97 | }
98 | }
99 | });
100 |
101 | var table = $("table.list.messages");
102 |
103 | $("