##// END OF EJS Templates
Merged r3668 from trunk....
Jean-Philippe Lang -
r3583:4a295e723e37
parent child
Show More
@@ -1,76 +1,78
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class VersionsController < ApplicationController
18 class VersionsController < ApplicationController
19 menu_item :roadmap
19 menu_item :roadmap
20 before_filter :find_version, :except => :close_completed
20 before_filter :find_version, :except => :close_completed
21 before_filter :find_project, :only => :close_completed
21 before_filter :find_project, :only => :close_completed
22 before_filter :authorize
22 before_filter :authorize
23
23
24 helper :custom_fields
24 helper :custom_fields
25 helper :projects
25 helper :projects
26
26
27 def show
27 def show
28 end
28 end
29
29
30 def edit
30 def edit
31 if request.post? && params[:version]
31 if request.post? && params[:version]
32 attributes = params[:version].dup
32 attributes = params[:version].dup
33 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
33 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
34 if @version.update_attributes(attributes)
34 if @version.update_attributes(attributes)
35 flash[:notice] = l(:notice_successful_update)
35 flash[:notice] = l(:notice_successful_update)
36 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
36 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
37 end
37 end
38 end
38 end
39 end
39 end
40
40
41 def close_completed
41 def close_completed
42 if request.post?
42 if request.post?
43 @project.close_completed_versions
43 @project.close_completed_versions
44 end
44 end
45 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
45 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
46 end
46 end
47
47
48 def destroy
48 def destroy
49 @version.destroy
49 if @version.fixed_issues.empty?
50 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
50 @version.destroy
51 rescue
51 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
52 flash[:error] = l(:notice_unable_delete_version)
52 else
53 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
53 flash[:error] = l(:notice_unable_delete_version)
54 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
55 end
54 end
56 end
55
57
56 def status_by
58 def status_by
57 respond_to do |format|
59 respond_to do |format|
58 format.html { render :action => 'show' }
60 format.html { render :action => 'show' }
59 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
61 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
60 end
62 end
61 end
63 end
62
64
63 private
65 private
64 def find_version
66 def find_version
65 @version = Version.find(params[:id])
67 @version = Version.find(params[:id])
66 @project = @version.project
68 @project = @version.project
67 rescue ActiveRecord::RecordNotFound
69 rescue ActiveRecord::RecordNotFound
68 render_404
70 render_404
69 end
71 end
70
72
71 def find_project
73 def find_project
72 @project = Project.find(params[:project_id])
74 @project = Project.find(params[:project_id])
73 rescue ActiveRecord::RecordNotFound
75 rescue ActiveRecord::RecordNotFound
74 render_404
76 render_404
75 end
77 end
76 end
78 end
@@ -1,209 +1,205
1 # redMine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
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
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.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Version < ActiveRecord::Base
18 class Version < ActiveRecord::Base
19 before_destroy :check_integrity
20 after_update :update_issues_from_sharing_change
19 after_update :update_issues_from_sharing_change
21 belongs_to :project
20 belongs_to :project
22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id'
21 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
23 acts_as_customizable
22 acts_as_customizable
24 acts_as_attachable :view_permission => :view_files,
23 acts_as_attachable :view_permission => :view_files,
25 :delete_permission => :manage_files
24 :delete_permission => :manage_files
26
25
27 VERSION_STATUSES = %w(open locked closed)
26 VERSION_STATUSES = %w(open locked closed)
28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
27 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
29
28
30 validates_presence_of :name
29 validates_presence_of :name
31 validates_uniqueness_of :name, :scope => [:project_id]
30 validates_uniqueness_of :name, :scope => [:project_id]
32 validates_length_of :name, :maximum => 60
31 validates_length_of :name, :maximum => 60
33 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
32 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
34 validates_inclusion_of :status, :in => VERSION_STATUSES
33 validates_inclusion_of :status, :in => VERSION_STATUSES
35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
34 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
36
35
37 named_scope :open, :conditions => {:status => 'open'}
36 named_scope :open, :conditions => {:status => 'open'}
38 named_scope :visible, lambda {|*args| { :include => :project,
37 named_scope :visible, lambda {|*args| { :include => :project,
39 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
38 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
40
39
41 # Returns true if +user+ or current user is allowed to view the version
40 # Returns true if +user+ or current user is allowed to view the version
42 def visible?(user=User.current)
41 def visible?(user=User.current)
43 user.allowed_to?(:view_issues, self.project)
42 user.allowed_to?(:view_issues, self.project)
44 end
43 end
45
44
46 def start_date
45 def start_date
47 effective_date
46 effective_date
48 end
47 end
49
48
50 def due_date
49 def due_date
51 effective_date
50 effective_date
52 end
51 end
53
52
54 # Returns the total estimated time for this version
53 # Returns the total estimated time for this version
55 def estimated_hours
54 def estimated_hours
56 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
55 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
57 end
56 end
58
57
59 # Returns the total reported time for this version
58 # Returns the total reported time for this version
60 def spent_hours
59 def spent_hours
61 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
60 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
62 end
61 end
63
62
64 def closed?
63 def closed?
65 status == 'closed'
64 status == 'closed'
66 end
65 end
67
66
68 def open?
67 def open?
69 status == 'open'
68 status == 'open'
70 end
69 end
71
70
72 # Returns true if the version is completed: due date reached and no open issues
71 # Returns true if the version is completed: due date reached and no open issues
73 def completed?
72 def completed?
74 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
73 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
75 end
74 end
76
75
77 # Returns the completion percentage of this version based on the amount of open/closed issues
76 # Returns the completion percentage of this version based on the amount of open/closed issues
78 # and the time spent on the open issues.
77 # and the time spent on the open issues.
79 def completed_pourcent
78 def completed_pourcent
80 if issues_count == 0
79 if issues_count == 0
81 0
80 0
82 elsif open_issues_count == 0
81 elsif open_issues_count == 0
83 100
82 100
84 else
83 else
85 issues_progress(false) + issues_progress(true)
84 issues_progress(false) + issues_progress(true)
86 end
85 end
87 end
86 end
88
87
89 # Returns the percentage of issues that have been marked as 'closed'.
88 # Returns the percentage of issues that have been marked as 'closed'.
90 def closed_pourcent
89 def closed_pourcent
91 if issues_count == 0
90 if issues_count == 0
92 0
91 0
93 else
92 else
94 issues_progress(false)
93 issues_progress(false)
95 end
94 end
96 end
95 end
97
96
98 # Returns true if the version is overdue: due date reached and some open issues
97 # Returns true if the version is overdue: due date reached and some open issues
99 def overdue?
98 def overdue?
100 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
99 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
101 end
100 end
102
101
103 # Returns assigned issues count
102 # Returns assigned issues count
104 def issues_count
103 def issues_count
105 @issue_count ||= fixed_issues.count
104 @issue_count ||= fixed_issues.count
106 end
105 end
107
106
108 # Returns the total amount of open issues for this version.
107 # Returns the total amount of open issues for this version.
109 def open_issues_count
108 def open_issues_count
110 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
109 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
111 end
110 end
112
111
113 # Returns the total amount of closed issues for this version.
112 # Returns the total amount of closed issues for this version.
114 def closed_issues_count
113 def closed_issues_count
115 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
114 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
116 end
115 end
117
116
118 def wiki_page
117 def wiki_page
119 if project.wiki && !wiki_page_title.blank?
118 if project.wiki && !wiki_page_title.blank?
120 @wiki_page ||= project.wiki.find_page(wiki_page_title)
119 @wiki_page ||= project.wiki.find_page(wiki_page_title)
121 end
120 end
122 @wiki_page
121 @wiki_page
123 end
122 end
124
123
125 def to_s; name end
124 def to_s; name end
126
125
127 # Versions are sorted by effective_date and name
126 # Versions are sorted by effective_date and name
128 # Those with no effective_date are at the end, sorted by name
127 # Those with no effective_date are at the end, sorted by name
129 def <=>(version)
128 def <=>(version)
130 if self.effective_date
129 if self.effective_date
131 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
130 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
132 else
131 else
133 version.effective_date ? 1 : (self.name <=> version.name)
132 version.effective_date ? 1 : (self.name <=> version.name)
134 end
133 end
135 end
134 end
136
135
137 # Returns the sharings that +user+ can set the version to
136 # Returns the sharings that +user+ can set the version to
138 def allowed_sharings(user = User.current)
137 def allowed_sharings(user = User.current)
139 VERSION_SHARINGS.select do |s|
138 VERSION_SHARINGS.select do |s|
140 if sharing == s
139 if sharing == s
141 true
140 true
142 else
141 else
143 case s
142 case s
144 when 'system'
143 when 'system'
145 # Only admin users can set a systemwide sharing
144 # Only admin users can set a systemwide sharing
146 user.admin?
145 user.admin?
147 when 'hierarchy', 'tree'
146 when 'hierarchy', 'tree'
148 # Only users allowed to manage versions of the root project can
147 # Only users allowed to manage versions of the root project can
149 # set sharing to hierarchy or tree
148 # set sharing to hierarchy or tree
150 project.nil? || user.allowed_to?(:manage_versions, project.root)
149 project.nil? || user.allowed_to?(:manage_versions, project.root)
151 else
150 else
152 true
151 true
153 end
152 end
154 end
153 end
155 end
154 end
156 end
155 end
157
156
158 private
157 private
159 def check_integrity
160 raise "Can't delete version" if self.fixed_issues.find(:first)
161 end
162
158
163 # Update the issue's fixed versions. Used if a version's sharing changes.
159 # Update the issue's fixed versions. Used if a version's sharing changes.
164 def update_issues_from_sharing_change
160 def update_issues_from_sharing_change
165 if sharing_changed?
161 if sharing_changed?
166 if VERSION_SHARINGS.index(sharing_was).nil? ||
162 if VERSION_SHARINGS.index(sharing_was).nil? ||
167 VERSION_SHARINGS.index(sharing).nil? ||
163 VERSION_SHARINGS.index(sharing).nil? ||
168 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
164 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
169 Issue.update_versions_from_sharing_change self
165 Issue.update_versions_from_sharing_change self
170 end
166 end
171 end
167 end
172 end
168 end
173
169
174 # Returns the average estimated time of assigned issues
170 # Returns the average estimated time of assigned issues
175 # or 1 if no issue has an estimated time
171 # or 1 if no issue has an estimated time
176 # Used to weigth unestimated issues in progress calculation
172 # Used to weigth unestimated issues in progress calculation
177 def estimated_average
173 def estimated_average
178 if @estimated_average.nil?
174 if @estimated_average.nil?
179 average = fixed_issues.average(:estimated_hours).to_f
175 average = fixed_issues.average(:estimated_hours).to_f
180 if average == 0
176 if average == 0
181 average = 1
177 average = 1
182 end
178 end
183 @estimated_average = average
179 @estimated_average = average
184 end
180 end
185 @estimated_average
181 @estimated_average
186 end
182 end
187
183
188 # Returns the total progress of open or closed issues. The returned percentage takes into account
184 # Returns the total progress of open or closed issues. The returned percentage takes into account
189 # the amount of estimated time set for this version.
185 # the amount of estimated time set for this version.
190 #
186 #
191 # Examples:
187 # Examples:
192 # issues_progress(true) => returns the progress percentage for open issues.
188 # issues_progress(true) => returns the progress percentage for open issues.
193 # issues_progress(false) => returns the progress percentage for closed issues.
189 # issues_progress(false) => returns the progress percentage for closed issues.
194 def issues_progress(open)
190 def issues_progress(open)
195 @issues_progress ||= {}
191 @issues_progress ||= {}
196 @issues_progress[open] ||= begin
192 @issues_progress[open] ||= begin
197 progress = 0
193 progress = 0
198 if issues_count > 0
194 if issues_count > 0
199 ratio = open ? 'done_ratio' : 100
195 ratio = open ? 'done_ratio' : 100
200
196
201 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
197 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
202 :include => :status,
198 :include => :status,
203 :conditions => ["is_closed = ?", !open]).to_f
199 :conditions => ["is_closed = ?", !open]).to_f
204 progress = done / (estimated_average * issues_count)
200 progress = done / (estimated_average * issues_count)
205 end
201 end
206 progress
202 progress
207 end
203 end
208 end
204 end
209 end
205 end
General Comments 0
You need to be logged in to leave comments. Login now