├── README.rdoc ├── app ├── controllers │ └── my_roadmaps_controller.rb ├── helpers │ └── my_roadmaps_helper.rb └── views │ └── my_roadmaps │ ├── _filters.html.erb │ ├── _related_issue.html.erb │ ├── _trackers_progress.html.erb │ └── index.html.erb ├── assets ├── screenshot │ └── latest_screenshot.png └── stylesheets │ └── trackers_progress.css ├── config ├── locales │ ├── en.yml │ ├── fr.yml │ └── zh.yml └── routes.rb ├── init.rb └── lib ├── issue_wrapper.rb ├── my_roadmaps └── hooks.rb ├── tracker_wrapper.rb └── version_synthesis.rb /README.rdoc: -------------------------------------------------------------------------------- 1 | = my_roadmaps 2 | 3 | This plugin provides global roadmaps for all the projects of the user. 4 | 5 | All your comments are greatly welcome: this is my first attempt at Ruby, Rails and Redmine plugin, so it most probably won't qualify as "state of the art". 6 | 7 | Please note that the main development branch (master) is for the Redmine 1.x. 8 | The other branches are for their respective version, up to the next one - Redmine_2.0 branch track Redmine 2.0.x compatibility, Redmine_2.1 branch is suitable for both Redmine 2.1 and 2.2 versions, Redmine_2.3 is for all 2.x versions including and after 2.3, Redmine_3.0 is for the 3.x version up to Redmine_3.4, which is compatible for Redmine 3.4. 9 | 10 | As usual, each version is available on Redmine plugin catalog: http://www.redmine.org/plugins/my_roadmaps 11 | 12 | It is licensed under the GPL v2. See http://www.gnu.org/licenses/old-licenses/gpl-2.0.html for further details. 13 | 14 | Here's a screenshot: https://github.com/clueware/redmine_my_roadmaps/raw/master/assets/screenshot/latest_screenshot.png 15 | -------------------------------------------------------------------------------- /app/controllers/my_roadmaps_controller.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # My Roadmaps - Redmine plugin to expose global roadmaps 3 | # Copyright (C) 2012 Stéphane Rondinaud 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | require 'version_synthesis' 20 | 21 | class MyRoadmapsController < ApplicationController 22 | unloadable 23 | before_filter :authorize_my_roadmaps 24 | 25 | helper :queries 26 | include QueriesHelper 27 | def index 28 | 29 | get_query 30 | 31 | @user_synthesis = Hash.new 32 | 33 | if @query.has_filter?('tracker_id') 34 | tracker_list = Tracker.find(:all, :is_in_roadmaps, :conditions => [@query.statement_for('tracker_id').gsub('issues.tracker_id','trackers.id')], :order => 'position') 35 | else 36 | tracker_list = Tracker.find(:all, :is_in_roadmaps, :order => 'position') 37 | end 38 | 39 | # condition hacked from the Query model to match versions 40 | version_condition = '(versions.status <> \'closed\')' 41 | version_condition += ' and ('+@query.statement_for('project_id').gsub('issues','versions')+' or exists (select 1 from issues where issues.fixed_version_id = versions.id and '+@query.statement_for('project_id')+'))' if @query.has_filter?('project_id') 42 | 43 | Version.find(:all, :conditions => [version_condition] ) \ 44 | .select{|version| !version.completed? } \ 45 | .each{|version| 46 | issue_condition = '' 47 | issue_condition += @query.statement_for('project_id')+' and ' unless @query.statement_for('project_id').nil? 48 | issue_condition += 'tracker_id in (?) and '+ \ 49 | '( fixed_version_id = ? '+ \ 50 | 'or exists (select 1 '+ \ 51 | 'from issues as subissues '+ \ 52 | 'where issues.root_id = subissues.root_id '+ \ 53 | 'and subissues.fixed_version_id = ?) )' 54 | 55 | issue_condition = [issue_condition, tracker_list, version.id, version.id] 56 | 57 | grouped_issues = Hash.new 58 | Issue.visible.find(:all, :conditions => issue_condition, :include => [:status,:tracker], :order => 'project_id,tracker_id' ) \ 59 | .each {|issue| 60 | if grouped_issues[issue.project].nil? 61 | grouped_issues[issue.project]=[issue] 62 | else 63 | grouped_issues[issue.project].push(issue) 64 | end 65 | } 66 | 67 | grouped_issues.each{|project, issues| 68 | if @user_synthesis[project].nil? 69 | @user_synthesis[project] = Hash.new 70 | end 71 | if @user_synthesis[project][version].nil? 72 | @user_synthesis[project][version] = VersionSynthesis.new(project, version, issues) 73 | else 74 | @user_synthesis[project][version].add_issues(issues) 75 | end 76 | } 77 | } 78 | end 79 | 80 | def initialize 81 | super 82 | index=0 83 | @tracker_styles = Hash.new 84 | Tracker.find(:all, :is_in_roadmaps, :order => 'position' ).each{ |tracker| 85 | @tracker_styles[tracker]=Hash.new 86 | @tracker_styles[tracker][:opened] = "t"+(index%10).to_s+"_opened" 87 | @tracker_styles[tracker][:done] = "t"+(index%10).to_s+"_done" 88 | @tracker_styles[tracker][:closed] = "t"+(index%10).to_s+"_closed" 89 | index += 1 90 | } 91 | end 92 | 93 | private 94 | 95 | def authorize_my_roadmaps 96 | if !(User.current.allowed_to?(:view_my_roadmaps, nil, :global => true) || User.current.admin?) 97 | render_403 98 | return false 99 | end 100 | return true 101 | end 102 | 103 | def get_query 104 | @query = Query.new(:name => "_", :filters => {}) 105 | user_projects = Project.visible 106 | user_trackers = Tracker.find(:all, :is_in_roadmaps) 107 | filters = Hash.new 108 | filters['project_id'] = { :type => :list_optional, :order => 1, :values => user_projects.sort{|a,b| a.self_and_ancestors.join('/')<=>b.self_and_ancestors.join('/') }.collect{|s| [s.self_and_ancestors.join('/'), s.id.to_s] } } unless user_projects.empty? 109 | filters['tracker_id'] = { :type => :list, :order => 2, :values => Tracker.find(:all, :is_in_roodmaps, :order => 'position' ).collect{|s| [s.name, s.id.to_s] } } unless user_trackers.empty? 110 | @query.override_available_filters(filters) 111 | if params[:f] 112 | build_query_from_params 113 | end 114 | @query.filters={ 'project_id' => {:operator => "*", :values => [""]} } if @query.filters.length==0 115 | end 116 | end 117 | -------------------------------------------------------------------------------- /app/helpers/my_roadmaps_helper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # My Roadmaps - Redmine plugin to expose global roadmaps 3 | # Copyright (C) 2012 Stéphane Rondinaud 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | require_dependency 'query' 20 | 21 | module MyRoadmapsHelper 22 | 23 | module VersionPatch 24 | def self.included(base) 25 | base.send(:include, InstanceMethods) 26 | end 27 | module InstanceMethods 28 | def splitted_version_name 29 | return self.name.split(/[^a-zA-Z0-9]/).compact.map{ |elem| 30 | (elem.to_i.to_s!=elem)?(elem.to_s):('%010d' % elem.to_i) 31 | } 32 | end 33 | end 34 | end 35 | 36 | module QueryPatch 37 | def self.included(base) 38 | base.send(:include, InstanceMethods) 39 | end 40 | 41 | module InstanceMethods 42 | def override_available_filters(new_filters=nil) 43 | @available_filters = new_filters if (!new_filters.nil? && new_filters.is_a?(Hash)) 44 | end 45 | 46 | def statement_for(field_name) 47 | # Copy/pasted from Query.statement 48 | # filters clauses 49 | filters_clauses = [] 50 | filters.each_key do |field| 51 | next if field == "subproject_id" || field != field_name 52 | v = values_for(field).clone 53 | next unless v and !v.empty? 54 | operator = operator_for(field) 55 | 56 | # "me" value substitution 57 | if %w(assigned_to_id author_id watcher_id).include?(field) 58 | if v.delete("me") 59 | if User.current.logged? 60 | v.push(User.current.id.to_s) 61 | v += User.current.group_ids.map(&:to_s) if field == 'assigned_to_id' 62 | else 63 | v.push("0") 64 | end 65 | end 66 | end 67 | 68 | if field =~ /^cf_(\d+)$/ 69 | # custom field 70 | filters_clauses << sql_for_custom_field(field, operator, v, $1) 71 | elsif respond_to?("sql_for_#{field}_field") 72 | # specific statement 73 | filters_clauses << send("sql_for_#{field}_field", field, operator, v) 74 | else 75 | # regular field 76 | filters_clauses << '(' + sql_for_field(field, operator, v, Issue.table_name, field) + ')' 77 | end 78 | end if filters and valid? 79 | 80 | filters_clauses << project_statement 81 | filters_clauses.reject!(&:blank?) 82 | 83 | filters_clauses.any? ? filters_clauses.join(' AND ') : nil 84 | end 85 | end 86 | end 87 | 88 | Query.send(:include, QueryPatch) 89 | Version.send(:include, VersionPatch) 90 | end 91 | -------------------------------------------------------------------------------- /app/views/my_roadmaps/_filters.html.erb: -------------------------------------------------------------------------------- 1 | <%# coding: UTF-8 %> 2 | <% form_tag({ :controller => 'my_roadmaps', :action => 'index'}, :method => :get, :id => 'query_form') do %> 3 | <%= hidden_field_tag 'set_filter', '1' %> 4 |
5 |
"> 6 | <%= l(:label_filter_plural) %> 7 |
"> 8 | <%= render :partial => 'queries/filters', :locals => {:query => query} %> 9 |
10 |
11 |
12 |

13 | <%= link_to_function l(:button_apply), "$('query_form').submit()", :class => 'icon icon-checked' %> 14 | <%= link_to l(:button_clear), { :set_filter => 0 }, :class => 'icon icon-reload' %> 15 |

16 | <% end %> 17 | -------------------------------------------------------------------------------- /app/views/my_roadmaps/_related_issue.html.erb: -------------------------------------------------------------------------------- 1 | <%# coding: UTF-8 %> 2 | <% 3 | issue = issue_wrapper.wrapped_issue 4 | if issue_wrapper.depth >0 5 | child_issue = " childissue" 6 | issue_div_style = 'style="margin-left: '+((issue_wrapper.depth-1)*10).to_s+'px;"' 7 | else 8 | child_issue = "" 9 | issue_div_style = "" 10 | end 11 | target_ratio = (issue.closed?)?100:issue.done_ratio 12 | if [0,100].include?(target_ratio) 13 | firstcolspan = synthesis.pct_done.length-2 14 | secondcolspan = 1 15 | else 16 | firstcolspan = synthesis.pct_done.index(target_ratio) 17 | secondcolspan = synthesis.pct_done.length-firstcolspan-1 18 | end 19 | if issue.closed? 20 | secondtdclass = firsttdclass = @tracker_styles[issue.tracker][:closed]+child_issue 21 | else 22 | if issue.done_ratio==0 23 | secondtdclass = firsttdclass = @tracker_styles[issue.tracker][:opened]+child_issue 24 | elsif issue.done_ratio==100 25 | secondtdclass = firsttdclass = @tracker_styles[issue.tracker][:done]+child_issue 26 | else 27 | firsttdclass = @tracker_styles[issue.tracker][:done]+child_issue 28 | secondtdclass = @tracker_styles[issue.tracker][:opened]+child_issue 29 | end 30 | end 31 | secondtdclass += " subtaskpct" unless issue.leaf? 32 | %> 33 | 34 | 35 | class="<%= 'tracker '+firsttdclass %>"> 36 |
> 37 | <%= link_to_issue(issue, :project => (synthesis.project != issue.project)) %> 38 | <%= " - "+link_to_user(issue.assigned_to) unless issue.assigned_to.nil? %> 39 |
40 | 41 | 1 %> class="<%= secondtdclass %>"> 42 | <%= (issue.closed?)?100:issue.done_ratio %>% 43 | 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /app/views/my_roadmaps/_trackers_progress.html.erb: -------------------------------------------------------------------------------- 1 | <%# coding: UTF-8 %> 2 | <% 3 | synthesis.trackers.each{ |tracker_wrapper| 4 | 5 | opened_left_pct = 100-tracker_wrapper.overall_done_pct.round 6 | opened_root_left_pct = 100-tracker_wrapper.overall_root_done_pct.round 7 | closed_tracker_style = @tracker_styles[tracker_wrapper.wrapped_tracker][:closed] 8 | done_tracker_style = @tracker_styles[tracker_wrapper.wrapped_tracker][:done] 9 | opened_tracker_style = @tracker_styles[tracker_wrapper.wrapped_tracker][:opened] 10 | %> 11 | 12 | <% if tracker_wrapper.closed_leaves+(tracker_wrapper.overall_done_pct-tracker_wrapper.closed_leaves_pct) > 0 %> 13 | 14 | <% if tracker_wrapper.closed_leaves.round > 0 %> 15 | 17 | <% end 18 | if (tracker_wrapper.overall_done_pct-tracker_wrapper.closed_leaves_pct).round > 0 %> 19 | 21 | <% end 22 | if opened_left_pct.round > 0 %> 23 | 25 | <% end %> 26 | 27 | <% else %> 28 | 29 | <% if tracker_wrapper.closed_root_nb.round > 0 %> 30 | 32 | <% end 33 | if (tracker_wrapper.overall_root_done_pct-tracker_wrapper.closed_root_pct).round > 0 %> 34 | 36 | <% end 37 | if opened_root_left_pct.round > 0 %> 38 | 40 | <% end %> 41 | 42 | <% end %> 43 |
44 |

"> 45 | <% if tracker_wrapper.closed_leaves+(tracker_wrapper.overall_done_pct-tracker_wrapper.closed_leaves_pct) > 0 %> 46 | <%= tracker_wrapper.wrapped_tracker.name+" "+(tracker_wrapper.overall_done_pct).round.to_s %>% 47 | <% else %> 48 | <%= tracker_wrapper.wrapped_tracker.name+" "+(tracker_wrapper.overall_root_done_pct).round.to_s %>% 49 | <% end %> 50 | <%=l(:label_for_root_issues, :count => tracker_wrapper.total_root_nb.round, :pct => (tracker_wrapper.overall_root_done_pct).round ) unless (tracker_wrapper.total_root_nb == tracker_wrapper.total_nb) %>

51 |

"> 52 | <%= link_to_if(tracker_wrapper.closed_nb > 0, 53 | l(:label_x_closed_issues_abbr, :count => tracker_wrapper.closed_nb), 54 | :controller => 'issues', 55 | :action => 'index', 56 | :project_id => synthesis.project, 57 | :tracker_id => tracker_wrapper.wrapped_tracker, 58 | :status_id => 'c', 59 | :fixed_version_id => synthesis.version, 60 | :set_filter => 1) + 61 | "("+tracker_wrapper.closed_pct.round.to_s+"%) "%> 62 | <%= link_to_if(tracker_wrapper.opened_nb > 0, 63 | l(:label_x_open_issues_abbr, :count => tracker_wrapper.opened_nb), 64 | :controller => 'issues', 65 | :action => 'index', 66 | :project_id => synthesis.project, 67 | :tracker_id => tracker_wrapper.wrapped_tracker, 68 | :status_id => 'o', 69 | :fixed_version_id => synthesis.version, 70 | :set_filter => 1)+ 71 | "("+tracker_wrapper.opened_pct.round.to_s+"%)" %> 72 |

73 | <% } # synthesis.trackers.each %> 74 | -------------------------------------------------------------------------------- /app/views/my_roadmaps/index.html.erb: -------------------------------------------------------------------------------- 1 | <%# coding: UTF-8 %> 2 | <% 3 | # My Roadmaps - Redmine plugin to expose global roadmaps 4 | # Copyright (C) 2012 Stéphane Rondinaud 5 | # 6 | # This program is free software; you can redistribute it and/or 7 | # modify it under the terms of the GNU General Public License 8 | # as published by the Free Software Foundation; either version 2 9 | # of the License, or (at your option) any later version. 10 | # 11 | # This program is distributed in the hope that it will be useful, 12 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 13 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 14 | # GNU General Public License for more details. 15 | # 16 | # You should have received a copy of the GNU General Public License 17 | # along with this program; if not, write to the Free Software 18 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 19 | %> 20 | <%= render :partial => 'filters', :locals => {:query => @query} %> 21 |

<%= l(:my_roadmaps_name) %>

22 |

<%=l(:label_lighter_subtasks)%>

23 |
24 | <% 25 | @user_synthesis.keys.sort_by{|project| project.self_and_ancestors.join('/') }.each{ |project| 26 | %> 27 |

28 | <%= link_to_if(project.visible?(User.current),project.name, {:controller => 'projects', :action => 'show', :id => project.id}) %> 29 |

30 | <% 31 | @user_synthesis[project].keys.sort{|v1,v2| v1.splitted_version_name<=>v2.splitted_version_name}.each{ |version| 32 | synthesis = @user_synthesis[project][version] 33 | %> 34 |

35 | <%= link_to_if(project.visible?(User.current),project.name, {:controller => 'projects', :action => 'show', :id => project.id}) %> - 36 | <%= link_to_if(version.visible?(User.current),version.name, {:controller => 'versions', :action => 'show', :id => version.id}) %> 37 | 38 | <%= "("+l(:version_status_locked)+")" if version.status == 'locked' %> 39 | <%= link_to l(:button_edit), {:controller => 'versions', :action => 'edit', :id => version.id} if version.visible?(User.current) %> 40 | 41 |

42 | 43 | <%= render :partial => 'trackers_progress', :locals => {:synthesis => synthesis} %> 44 | 45 | <% if !synthesis.pct_done.nil? %> 46 | 47 | 50 | 51 | 63 | <% 64 | synthesis.issues.sort.each { |issue_wrapper| 65 | %> 66 | <%= render :partial => 'related_issue', :locals => {:synthesis => synthesis, :issue_wrapper => issue_wrapper } %> 67 | <% 68 | } 69 | %> 70 | 71 |
48 | <%= l(:label_related_issues) %> 49 |
52 | <% 53 | synthesis.pct_done.each_index{|i| 54 | if i>0 55 | %> 56 | 57 | <% 58 | end 59 | } 60 | %> 61 | 62 |
72 | <% end %> 73 |

74 | <% } # version %> 75 | <% } # project %> 76 |

<%= l(:my_roadmaps_no_version_match) if @user_synthesis.empty? %>

77 |
78 | -------------------------------------------------------------------------------- /assets/screenshot/latest_screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/clueware/redmine_my_roadmaps/171461322c0c73c762a51ed53c21fb8be9f6c796/assets/screenshot/latest_screenshot.png -------------------------------------------------------------------------------- /assets/stylesheets/trackers_progress.css: -------------------------------------------------------------------------------- 1 | div#roadmaps p { 2 | margin-top: 5px; 3 | margin-bottom: 5px; 4 | } 5 | 6 | h2.project { 7 | text-align: center; 8 | width: 100%; 9 | border-bottom: 1px solid black; 10 | margin-top: 1em; 11 | } 12 | table.tracker tr td { 13 | white-space: nowrap; 14 | text-align: right; 15 | overflow: visible; 16 | } 17 | table.tracker tr td div { 18 | position: absolute; 19 | text-align: left; 20 | left: 1.5em; 21 | } 22 | 23 | td.childissue div { 24 | padding-left: 16px; 25 | background: url("../../../images/bullet_arrow_right.png") no-repeat scroll 0 50% transparent; 26 | } 27 | 28 | p.subtaskpct, td.subtaskpct { 29 | color: #999999 30 | } 31 | 32 | table.progress { 33 | border: 1px solid #D7D7D7; 34 | } 35 | 36 | .t0_closed { background-color: #4DCC33 } 37 | .t0_done { background-color: #A6FF8C } 38 | .t0_opened { background-color: #FFFFFF } 39 | 40 | .t1_closed { background-color: #F55F79 } 41 | .t1_done { background-color: #FF8CA6 } 42 | .t1_opened { background-color: #FFFFFF } 43 | 44 | .t2_closed { background-color: #8CA6FF } 45 | .t2_done { background-color: #CCE6FF } 46 | .t2_opened { background-color: #FFFFFF } 47 | 48 | .t3_closed { background-color: #CC6633 } 49 | .t3_done { background-color: #FFBF8C } 50 | .t3_opened { background-color: #FFFFFF } 51 | 52 | .t4_closed { background-color: #99CC33 } 53 | .t4_done { background-color: #CCFF66 } 54 | .t4_opened { background-color: #FFFFFF } 55 | 56 | .t5_closed { background-color: #B333CC } 57 | .t5_done { background-color: #FF8CFF } 58 | .t5_opened { background-color: #FFFFFF } 59 | 60 | .t6_closed { background-color: #95AAC4 } 61 | .t6_done { background-color: #D0E5FF } 62 | .t6_opened { background-color: #FFFFFF } 63 | 64 | .t7_closed { background-color: #886B64 } 65 | .t7_done { background-color: #FFE2DB } 66 | .t7_opened { background-color: #FFFFFF } 67 | 68 | .t8_closed { background-color: #3399CC } 69 | .t8_done { background-color: #66CCFF } 70 | .t8_opened { background-color: #FFFFFF } 71 | 72 | .t9_closed { background-color: #CC6699 } 73 | .t9_done { background-color: #FF99CC } 74 | .t9_opened { background-color: #FFFFFF } 75 | -------------------------------------------------------------------------------- /config/locales/en.yml: -------------------------------------------------------------------------------- 1 | # My Roadmaps - Redmine plugin to expose global roadmaps 2 | # Copyright (C) 2012 Stéphane Rondinaud 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | # English strings go here for Rails i18n 19 | en: 20 | my_roadmaps_name: My roadmaps 21 | my_roadmaps_no_version_match: No version matching criteria. 22 | permission_view_my_roadmaps: View My Roadmaps 23 | label_lighter_subtasks: Lighter labels indicates subtasking with no direct impact on progress. 24 | label_for_root_issues: 25 | zero: "(no root issue)" 26 | one: "(%{pct}%% for root issue)" 27 | other: "(%{pct}%% for %{count} root issues)" 28 | button_refresh: Refresh 29 | -------------------------------------------------------------------------------- /config/locales/fr.yml: -------------------------------------------------------------------------------- 1 | # My Roadmaps - Redmine plugin to expose global roadmaps 2 | # Copyright (C) 2012 Stéphane Rondinaud 3 | # 4 | # This program is free software; you can redistribute it and/or 5 | # modify it under the terms of the GNU General Public License 6 | # as published by the Free Software Foundation; either version 2 7 | # of the License, or (at your option) any later version. 8 | # 9 | # This program is distributed in the hope that it will be useful, 10 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | # GNU General Public License for more details. 13 | # 14 | # You should have received a copy of the GNU General Public License 15 | # along with this program; if not, write to the Free Software 16 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 17 | 18 | # French strings go here for Rails i18n 19 | fr: 20 | my_roadmaps_name: Mes roadmaps 21 | my_roadmaps_no_version_match: Aucune version ne correspond au critère. 22 | permission_view_my_roadmaps: Voir "Mes Roadmaps" 23 | label_lighter_subtasks: Un libellé plus clair indique des sous-tâches n'ayant pas d'impact direct sur la progression. 24 | label_for_root_issues: 25 | zero: "(pas de demande racine)" 26 | one: "(%{pct}%% pour la demande racine)" 27 | other: "(%{pct}%% pour les %{count} demandes racines)" 28 | button_refresh: Rafraichir 29 | -------------------------------------------------------------------------------- /config/locales/zh.yml: -------------------------------------------------------------------------------- 1 | # Chinese strings goes here, courtesy of https://github.com/archonwang 2 | zh: 3 | my_roadmaps_name: 我的路线图 4 | permission_view_my_roadmaps: 查看我的路线图 5 | label_lighter_subtasks: 浅色的标签显示子任务没有直接影响进度。 6 | label_for_root_issues: 7 | zero: "(没有根本问题)" 8 | one: "(%{pct}%%个根本问题)" 9 | other: "(第%{pct}%%个根本问题,共%{count}个)" 10 | button_refresh: 刷新 11 | -------------------------------------------------------------------------------- /config/routes.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | ActionController::Routing::Routes.draw do |map| 3 | map.connect "/my_roadmaps", :controller => "my_roadmaps" 4 | end 5 | -------------------------------------------------------------------------------- /init.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # My Roadmaps - Redmine plugin to expose global roadmaps 3 | # Copyright (C) 2012 Stéphane Rondinaud 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | require 'redmine' 20 | 21 | require_dependency 'my_roadmaps/hooks' 22 | 23 | Redmine::Plugin.register :redmine_my_roadmaps do 24 | name 'My Roadmaps plugin' 25 | author 'Stéphane Rondinaud' 26 | description 'This plugin provides a global roadmaps for all the projects of the user.' 27 | version '0.1.12' 28 | url 'https://github.com/clueware/redmine_my_roadmaps' 29 | 30 | permission :view_my_roadmaps, :my_roadmaps => :index 31 | menu :top_menu, :my_roadmaps, { :controller => 'my_roadmaps', :action => 'index' }, :caption => :my_roadmaps_name, :if => Proc.new { User.current.allowed_to?(:view_my_roadmaps, nil, :global => true) } 32 | end 33 | -------------------------------------------------------------------------------- /lib/issue_wrapper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # My Roadmaps - Redmine plugin to expose global roadmaps 3 | # Copyright (C) 2012 Stéphane Rondinaud 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | class IssueWrapper 20 | def initialize(wrapped_issue, depth) 21 | @wrapped_issue = wrapped_issue 22 | @depth = depth 23 | end 24 | 25 | def <=>(other) 26 | [((self.wrapped_issue.root_id==self.wrapped_issue.id)?(self.wrapped_issue.tracker):(self.wrapped_issue.root.tracker)), \ 27 | self.wrapped_issue.root_id, \ 28 | self.wrapped_issue.self_and_ancestors.to_a, \ 29 | self.wrapped_issue.tracker, \ 30 | self.wrapped_issue.id] \ 31 | <=> \ 32 | [((other.wrapped_issue.root_id==other.wrapped_issue.id)?(other.wrapped_issue.tracker):(other.wrapped_issue.root.tracker)), \ 33 | other.wrapped_issue.root_id, \ 34 | other.wrapped_issue.self_and_ancestors.to_a, \ 35 | other.wrapped_issue.tracker, \ 36 | other.wrapped_issue.id] 37 | end 38 | 39 | attr_reader :wrapped_issue, :depth 40 | end 41 | -------------------------------------------------------------------------------- /lib/my_roadmaps/hooks.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # My Roadmaps - Redmine plugin to expose global roadmaps 3 | # Copyright (C) 2012 Stéphane Rondinaud 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | require 'redmine' 20 | 21 | module MyRoadmaps 22 | class Hooks < Redmine::Hook::ViewListener 23 | def view_layouts_base_html_head(context) 24 | stylesheet_link_tag 'trackers_progress', :plugin => :redmine_my_roadmaps 25 | end 26 | end 27 | end 28 | -------------------------------------------------------------------------------- /lib/tracker_wrapper.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # My Roadmaps - Redmine plugin to expose global roadmaps 3 | # Copyright (C) 2012 Stéphane Rondinaud 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | class TrackerWrapper 20 | def initialize(version, tracker) 21 | @wrapped_tracker = tracker 22 | @version = version 23 | # total issue number 24 | @total_nb = 0 25 | # total leaves issue number 26 | @total_leaves = 0 27 | # closed issue number 28 | @closed_nb = 0 29 | # closed leaves issue number 30 | @closed_leaves = 0 31 | # number of issues presenting a completion ratio > 0, excluding closed ones 32 | @done_nb = 0 33 | # average completion of the @done_nb issues 34 | @done_pct = 0.0 35 | # opened issues number 36 | @opened_nb = 0 37 | # opened leaves issues number 38 | @opened_leaves = 0 39 | # % of closed issues 40 | @closed_pct = 0 41 | # % of closed leaves issues 42 | @closed_leaves_pct = 0 43 | # % of opened issues 44 | @opened_pct = 0 45 | # total number of root issues for the wrapped tracker 46 | @total_root_nb = 0 47 | # closed root issue number 48 | @closed_root_nb = 0 49 | # % of closed root issues 50 | @closed_root_pct = 0 51 | # number of root issues presenting a completion ratio > 0, excluding closed ones 52 | @done_root_nb = 0 53 | # average completion of the @done_root_nb issues 54 | @done_root_pct = 0.0 55 | 56 | # @done_root_pct accumulator 57 | @sum_done_root_pct = 0 58 | # @done_pct accumulator 59 | @sum_done_pct = 0 60 | 61 | @is_subtasks = true 62 | end 63 | 64 | # adds a new issue to the statistics 65 | def addIssue(issue) 66 | 67 | # global stats 68 | @total_nb += 1 69 | @closed_nb += 1 if issue.closed? 70 | if (issue.leaf?) 71 | clear_tracker_subtask_status 72 | @total_leaves += 1 73 | @closed_leaves += 1 if issue.closed? 74 | if issue.done_ratio > 0 && !issue.closed? 75 | @sum_done_pct += issue.done_ratio 76 | @done_nb += 1 77 | end 78 | end 79 | 80 | # root issues stats 81 | if issue.root_id == issue.id 82 | @closed_root_nb += 1 if issue.closed? 83 | @total_root_nb += 1 84 | if issue.done_ratio > 0 && !issue.closed? 85 | @sum_done_root_pct += issue.done_ratio 86 | @done_root_nb += 1 87 | end 88 | end 89 | 90 | # subtasks with only subtasks should be accounted for as long as the tracker 91 | # itself does not contain leaves issues or root issues 92 | # The "leaves" statistics then contains the relevant information 93 | if @is_subtasks && (issue.root_id!=issue.id) && !issue.leaf? 94 | @total_leaves += 1 95 | @closed_leaves += 1 if issue.closed? 96 | if issue.done_ratio > 0 && !issue.closed? 97 | @sum_done_pct += issue.done_ratio 98 | @done_nb += 1 99 | end 100 | end 101 | 102 | if @done_nb>0 103 | @done_pct = @sum_done_pct.to_f/@done_nb.to_f 104 | else 105 | @done_pct = 0 106 | end 107 | 108 | if @done_root_nb>0 109 | @done_root_pct = @sum_done_root_pct.to_f/@done_root_nb.to_f 110 | else 111 | @done_root_pct = 0 112 | end 113 | 114 | @opened_nb = @total_nb - @closed_nb 115 | @opened_leaves = @total_leaves - @closed_leaves 116 | if @total_nb>0 117 | @closed_pct = @closed_nb.to_f*100.0/@total_nb.to_f 118 | @opened_pct = @opened_nb.to_f*100.0/@total_nb.to_f 119 | @closed_leaves_pct = @closed_leaves.to_f*100.0/@total_leaves.to_f 120 | else 121 | @closed_pct = 0 122 | @opened_pct = 0 123 | @closed_leaves_pct = 0 124 | end 125 | 126 | if @total_root_nb>0 127 | @closed_root_pct = @closed_root_nb.to_f*100.0/@total_root_nb.to_f unless @total_root_nb == 0 128 | else 129 | @closed_root_pct = 0 130 | end 131 | end 132 | 133 | # clear the is_subtasks flag and reset simulated leaves statistics 134 | # when a tracker has a "real" issue 135 | def clear_tracker_subtask_status 136 | if @is_subtasks 137 | @is_subtasks = false 138 | @total_leaves = 0 139 | @closed_leaves = 0 140 | @closed_leaves_pct = 0 141 | @opened_leaves = 0 142 | @done_nb = 0 143 | @done_pct = 0 144 | @sum_done_pct = 0 145 | end 146 | end 147 | 148 | # returns the overall root issues done %, taking into account closed root issues and % done 149 | def overall_root_done_pct 150 | if @total_root_nb == 0 151 | result = 0 152 | else 153 | result = (@closed_root_nb.to_f*100.0 + @done_root_nb.to_f*@done_root_pct).to_f/@total_root_nb.to_f 154 | end 155 | return result 156 | end 157 | 158 | # returns the overall done %, taking into account closed issues and % done 159 | def overall_done_pct 160 | if @total_leaves == 0 161 | result = 0 162 | else 163 | result = (@closed_leaves.to_f*100.0 + @done_nb.to_f*@done_pct).to_f/@total_leaves.to_f 164 | end 165 | return result 166 | end 167 | 168 | attr_reader :wrapped_tracker, :total_nb, :total_root_nb, :total_leaves 169 | attr_reader :closed_nb, :closed_leaves, :closed_root_nb, :done_root_nb, :done_nb, :opened_nb, :opened_leaves 170 | attr_reader :closed_pct, :closed_leaves_pct, :closed_root_pct, :done_root_pct, :done_pct, :opened_pct 171 | attr_reader :is_subtasks 172 | end 173 | -------------------------------------------------------------------------------- /lib/version_synthesis.rb: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # My Roadmaps - Redmine plugin to expose global roadmaps 3 | # Copyright (C) 2012 Stéphane Rondinaud 4 | # 5 | # This program is free software; you can redistribute it and/or 6 | # modify it under the terms of the GNU General Public License 7 | # as published by the Free Software Foundation; either version 2 8 | # of the License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU General Public License 16 | # along with this program; if not, write to the Free Software 17 | # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. 18 | 19 | class VersionSynthesis 20 | def initialize(project, version, issues) 21 | @version = version 22 | @project = project 23 | @issues = Array.new 24 | 25 | @trackers = Array.new 26 | 27 | @max_depth=0 28 | self.add_issues(issues) 29 | end 30 | 31 | def add_issues(issues) 32 | issues.each{ |issue| 33 | if @trackers.select{|wrapper| wrapper.wrapped_tracker == issue.tracker }.length!=1 34 | current_tracker = TrackerWrapper.new(version,issue.tracker) 35 | @trackers.push(current_tracker) 36 | else 37 | current_tracker = @trackers.select{|wrapper| wrapper.wrapped_tracker == issue.tracker }[0] 38 | end 39 | current_tracker.addIssue(issue) 40 | depth=issues.select{|iss| (iss.lftissue.rgt) && (iss.id!=issue.id) && (iss.root_id==issue.root_id) }.length 41 | @issues.push(IssueWrapper.new(issue,depth)) 42 | if @max_depthb.wrapped_tracker.position} 48 | 49 | @done_pct = 0 50 | @done_nb = 0 51 | @total_nb = 0 52 | @closed_nb = 0 53 | @opened_nb = 0 54 | @closed_pct = 0 55 | @opened_pct = 0 56 | @trackers.each{|wrapper| 57 | @total_nb += wrapper.total_nb 58 | @closed_nb += wrapper.closed_nb 59 | @opened_nb += wrapper.opened_nb 60 | @done_nb += wrapper.done_nb 61 | @done_pct += wrapper.done_pct 62 | } 63 | if @done_nb > 0 64 | @done_pct /= @done_nb 65 | end 66 | if @total_nb > 0 67 | @closed_pct = @closed_nb*100/@total_nb 68 | @opened_pct = @opened_nb*100/@total_nb 69 | end 70 | 71 | # issues progress % set 72 | if !@issues.nil? && @issues.length > 0 73 | @pct_done = @issues.map{|issue_wrapper| (issue_wrapper.wrapped_issue.closed?)?100:issue_wrapper.wrapped_issue.done_ratio} 74 | @pct_done.push(0,100) 75 | @pct_done.uniq! 76 | @pct_done.sort! 77 | # when all issues are either not started or closed 78 | @pct_done = [0,90,100] if pct_done.length==2 79 | else 80 | @pct_done = nil 81 | end 82 | end 83 | 84 | attr_reader :version, :project, :max_depth, :issues, :trackers, :pct_done, :done_nb, :done_pct, :closed_nb, :closed_pct, :opened_nb, :opened_pct 85 | end 86 | --------------------------------------------------------------------------------