├── 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 |
4 |
5 |
-
6 |
7 |
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 |
3 |

<%= l(:label_vote_result) %>

4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | <% @message.each do |topic| %> 15 | 16 | 17 | 18 | 19 | 20 | 21 | 27 | 28 | <% end %> 29 | 30 |
<%= l(:field_subject) %><%= l(:field_author) %><%= l(:field_created_on) %><%= l(:label_vote_count) %><%= l(:label_reply_plural) %><%= l(:label_message_last) %>
<%= link_to h(topic.subject), board_message_path(@board, topic) %><%= link_to_user(topic.author) %><%= format_time(topic.created_on) %><%= @votes_message[topic.id] %><%= topic.replies_count %> 22 | <% if topic.last_reply %> 23 | <%= authoring topic.last_reply.created_on, topic.last_reply.author %>
24 | <%= link_to_message topic.last_reply %> 25 | <% end %> 26 |
31 |
32 | <% end %> -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | = Redmine Vote 2 | {Dependency Status}[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 | {Screenshot}[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 | $("") 104 | .html($("#label_vote_count").text()) 105 | .insertAfter(table.find("thead > tr > th:nth-child(3)")); 106 | 107 | $("") 108 | .addClass("vote-td-trigger") 109 | .insertAfter(table.find("tbody > tr > td:nth-child(3)")); 110 | 111 | $("td.vote-td-trigger").each(function() { 112 | var _re = /\/boards\/([0-9]*)\/topics\/([0-9]*)/; 113 | var _href = $(this).parent().find("td.subject > a").attr("href"); 114 | var _match = _href.match(_re); 115 | if(_match) { 116 | var _board = _match[1]; 117 | var _topic = _match[2]; 118 | var _this = $(this); 119 | $.ajax({ 120 | type: "GET", 121 | url: base + "boards/" + _board + "/topics/" + _topic + "/vote", 122 | cache: false, 123 | success: function(data, textStatus, jqXHR) { 124 | _this.html(data.point); 125 | } 126 | }); 127 | } 128 | }); 129 | } 130 | }); 131 | --------------------------------------------------------------------------------