├── 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 |
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_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 |
--------------------------------------------------------------------------------