##// END OF EJS Templates
Do not raise an error when destroying a Version with assigned issues....
Jean-Philippe Lang -
r3554:9ccccb998485
parent child
Show More
@@ -1,103 +1,105
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 model_object Version
20 model_object Version
21 before_filter :find_model_object, :except => [:new, :close_completed]
21 before_filter :find_model_object, :except => [:new, :close_completed]
22 before_filter :find_project_from_association, :except => [:new, :close_completed]
22 before_filter :find_project_from_association, :except => [:new, :close_completed]
23 before_filter :find_project, :only => [:new, :close_completed]
23 before_filter :find_project, :only => [:new, :close_completed]
24 before_filter :authorize
24 before_filter :authorize
25
25
26 helper :custom_fields
26 helper :custom_fields
27 helper :projects
27 helper :projects
28
28
29 def show
29 def show
30 end
30 end
31
31
32 def new
32 def new
33 @version = @project.versions.build
33 @version = @project.versions.build
34 if params[:version]
34 if params[:version]
35 attributes = params[:version].dup
35 attributes = params[:version].dup
36 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
36 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
37 @version.attributes = attributes
37 @version.attributes = attributes
38 end
38 end
39 if request.post?
39 if request.post?
40 if @version.save
40 if @version.save
41 respond_to do |format|
41 respond_to do |format|
42 format.html do
42 format.html do
43 flash[:notice] = l(:notice_successful_create)
43 flash[:notice] = l(:notice_successful_create)
44 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
44 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
45 end
45 end
46 format.js do
46 format.js do
47 # IE doesn't support the replace_html rjs method for select box options
47 # IE doesn't support the replace_html rjs method for select box options
48 render(:update) {|page| page.replace "issue_fixed_version_id",
48 render(:update) {|page| page.replace "issue_fixed_version_id",
49 content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
49 content_tag('select', '<option></option>' + version_options_for_select(@project.shared_versions.open, @version), :id => 'issue_fixed_version_id', :name => 'issue[fixed_version_id]')
50 }
50 }
51 end
51 end
52 end
52 end
53 else
53 else
54 respond_to do |format|
54 respond_to do |format|
55 format.html
55 format.html
56 format.js do
56 format.js do
57 render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
57 render(:update) {|page| page.alert(@version.errors.full_messages.join('\n')) }
58 end
58 end
59 end
59 end
60 end
60 end
61 end
61 end
62 end
62 end
63
63
64 def edit
64 def edit
65 if request.post? && params[:version]
65 if request.post? && params[:version]
66 attributes = params[:version].dup
66 attributes = params[:version].dup
67 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
67 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
68 if @version.update_attributes(attributes)
68 if @version.update_attributes(attributes)
69 flash[:notice] = l(:notice_successful_update)
69 flash[:notice] = l(:notice_successful_update)
70 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
70 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
71 end
71 end
72 end
72 end
73 end
73 end
74
74
75 def close_completed
75 def close_completed
76 if request.post?
76 if request.post?
77 @project.close_completed_versions
77 @project.close_completed_versions
78 end
78 end
79 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
79 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
80 end
80 end
81
81
82 def destroy
82 def destroy
83 if @version.fixed_issues.empty?
83 @version.destroy
84 @version.destroy
84 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
85 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
85 rescue
86 else
86 flash[:error] = l(:notice_unable_delete_version)
87 flash[:error] = l(:notice_unable_delete_version)
87 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
88 redirect_to :controller => 'projects', :action => 'settings', :tab => 'versions', :id => @project
88 end
89 end
90 end
89
91
90 def status_by
92 def status_by
91 respond_to do |format|
93 respond_to do |format|
92 format.html { render :action => 'show' }
94 format.html { render :action => 'show' }
93 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
95 format.js { render(:update) {|page| page.replace_html 'status_by', render_issue_status_by(@version, params[:status_by])} }
94 end
96 end
95 end
97 end
96
98
97 private
99 private
98 def find_project
100 def find_project
99 @project = Project.find(params[:project_id])
101 @project = Project.find(params[:project_id])
100 rescue ActiveRecord::RecordNotFound
102 rescue ActiveRecord::RecordNotFound
101 render_404
103 render_404
102 end
104 end
103 end
105 end
@@ -1,210 +1,206
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 # (sum of leaves estimated_hours)
54 # (sum of leaves estimated_hours)
56 def estimated_hours
55 def estimated_hours
57 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
56 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
58 end
57 end
59
58
60 # Returns the total reported time for this version
59 # Returns the total reported time for this version
61 def spent_hours
60 def spent_hours
62 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
61 @spent_hours ||= TimeEntry.sum(:hours, :include => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
63 end
62 end
64
63
65 def closed?
64 def closed?
66 status == 'closed'
65 status == 'closed'
67 end
66 end
68
67
69 def open?
68 def open?
70 status == 'open'
69 status == 'open'
71 end
70 end
72
71
73 # Returns true if the version is completed: due date reached and no open issues
72 # Returns true if the version is completed: due date reached and no open issues
74 def completed?
73 def completed?
75 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
74 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
76 end
75 end
77
76
78 # Returns the completion percentage of this version based on the amount of open/closed issues
77 # Returns the completion percentage of this version based on the amount of open/closed issues
79 # and the time spent on the open issues.
78 # and the time spent on the open issues.
80 def completed_pourcent
79 def completed_pourcent
81 if issues_count == 0
80 if issues_count == 0
82 0
81 0
83 elsif open_issues_count == 0
82 elsif open_issues_count == 0
84 100
83 100
85 else
84 else
86 issues_progress(false) + issues_progress(true)
85 issues_progress(false) + issues_progress(true)
87 end
86 end
88 end
87 end
89
88
90 # Returns the percentage of issues that have been marked as 'closed'.
89 # Returns the percentage of issues that have been marked as 'closed'.
91 def closed_pourcent
90 def closed_pourcent
92 if issues_count == 0
91 if issues_count == 0
93 0
92 0
94 else
93 else
95 issues_progress(false)
94 issues_progress(false)
96 end
95 end
97 end
96 end
98
97
99 # Returns true if the version is overdue: due date reached and some open issues
98 # Returns true if the version is overdue: due date reached and some open issues
100 def overdue?
99 def overdue?
101 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
100 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
102 end
101 end
103
102
104 # Returns assigned issues count
103 # Returns assigned issues count
105 def issues_count
104 def issues_count
106 @issue_count ||= fixed_issues.count
105 @issue_count ||= fixed_issues.count
107 end
106 end
108
107
109 # Returns the total amount of open issues for this version.
108 # Returns the total amount of open issues for this version.
110 def open_issues_count
109 def open_issues_count
111 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
110 @open_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, false], :include => :status)
112 end
111 end
113
112
114 # Returns the total amount of closed issues for this version.
113 # Returns the total amount of closed issues for this version.
115 def closed_issues_count
114 def closed_issues_count
116 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
115 @closed_issues_count ||= Issue.count(:all, :conditions => ["fixed_version_id = ? AND is_closed = ?", self.id, true], :include => :status)
117 end
116 end
118
117
119 def wiki_page
118 def wiki_page
120 if project.wiki && !wiki_page_title.blank?
119 if project.wiki && !wiki_page_title.blank?
121 @wiki_page ||= project.wiki.find_page(wiki_page_title)
120 @wiki_page ||= project.wiki.find_page(wiki_page_title)
122 end
121 end
123 @wiki_page
122 @wiki_page
124 end
123 end
125
124
126 def to_s; name end
125 def to_s; name end
127
126
128 # Versions are sorted by effective_date and name
127 # Versions are sorted by effective_date and name
129 # Those with no effective_date are at the end, sorted by name
128 # Those with no effective_date are at the end, sorted by name
130 def <=>(version)
129 def <=>(version)
131 if self.effective_date
130 if self.effective_date
132 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
131 version.effective_date ? (self.effective_date == version.effective_date ? self.name <=> version.name : self.effective_date <=> version.effective_date) : -1
133 else
132 else
134 version.effective_date ? 1 : (self.name <=> version.name)
133 version.effective_date ? 1 : (self.name <=> version.name)
135 end
134 end
136 end
135 end
137
136
138 # Returns the sharings that +user+ can set the version to
137 # Returns the sharings that +user+ can set the version to
139 def allowed_sharings(user = User.current)
138 def allowed_sharings(user = User.current)
140 VERSION_SHARINGS.select do |s|
139 VERSION_SHARINGS.select do |s|
141 if sharing == s
140 if sharing == s
142 true
141 true
143 else
142 else
144 case s
143 case s
145 when 'system'
144 when 'system'
146 # Only admin users can set a systemwide sharing
145 # Only admin users can set a systemwide sharing
147 user.admin?
146 user.admin?
148 when 'hierarchy', 'tree'
147 when 'hierarchy', 'tree'
149 # Only users allowed to manage versions of the root project can
148 # Only users allowed to manage versions of the root project can
150 # set sharing to hierarchy or tree
149 # set sharing to hierarchy or tree
151 project.nil? || user.allowed_to?(:manage_versions, project.root)
150 project.nil? || user.allowed_to?(:manage_versions, project.root)
152 else
151 else
153 true
152 true
154 end
153 end
155 end
154 end
156 end
155 end
157 end
156 end
158
157
159 private
158 private
160 def check_integrity
161 raise "Can't delete version" if self.fixed_issues.find(:first)
162 end
163
159
164 # Update the issue's fixed versions. Used if a version's sharing changes.
160 # Update the issue's fixed versions. Used if a version's sharing changes.
165 def update_issues_from_sharing_change
161 def update_issues_from_sharing_change
166 if sharing_changed?
162 if sharing_changed?
167 if VERSION_SHARINGS.index(sharing_was).nil? ||
163 if VERSION_SHARINGS.index(sharing_was).nil? ||
168 VERSION_SHARINGS.index(sharing).nil? ||
164 VERSION_SHARINGS.index(sharing).nil? ||
169 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
165 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
170 Issue.update_versions_from_sharing_change self
166 Issue.update_versions_from_sharing_change self
171 end
167 end
172 end
168 end
173 end
169 end
174
170
175 # Returns the average estimated time of assigned issues
171 # Returns the average estimated time of assigned issues
176 # or 1 if no issue has an estimated time
172 # or 1 if no issue has an estimated time
177 # Used to weigth unestimated issues in progress calculation
173 # Used to weigth unestimated issues in progress calculation
178 def estimated_average
174 def estimated_average
179 if @estimated_average.nil?
175 if @estimated_average.nil?
180 average = fixed_issues.average(:estimated_hours).to_f
176 average = fixed_issues.average(:estimated_hours).to_f
181 if average == 0
177 if average == 0
182 average = 1
178 average = 1
183 end
179 end
184 @estimated_average = average
180 @estimated_average = average
185 end
181 end
186 @estimated_average
182 @estimated_average
187 end
183 end
188
184
189 # Returns the total progress of open or closed issues. The returned percentage takes into account
185 # Returns the total progress of open or closed issues. The returned percentage takes into account
190 # the amount of estimated time set for this version.
186 # the amount of estimated time set for this version.
191 #
187 #
192 # Examples:
188 # Examples:
193 # issues_progress(true) => returns the progress percentage for open issues.
189 # issues_progress(true) => returns the progress percentage for open issues.
194 # issues_progress(false) => returns the progress percentage for closed issues.
190 # issues_progress(false) => returns the progress percentage for closed issues.
195 def issues_progress(open)
191 def issues_progress(open)
196 @issues_progress ||= {}
192 @issues_progress ||= {}
197 @issues_progress[open] ||= begin
193 @issues_progress[open] ||= begin
198 progress = 0
194 progress = 0
199 if issues_count > 0
195 if issues_count > 0
200 ratio = open ? 'done_ratio' : 100
196 ratio = open ? 'done_ratio' : 100
201
197
202 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
198 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
203 :include => :status,
199 :include => :status,
204 :conditions => ["is_closed = ?", !open]).to_f
200 :conditions => ["is_closed = ?", !open]).to_f
205 progress = done / (estimated_average * issues_count)
201 progress = done / (estimated_average * issues_count)
206 end
202 end
207 progress
203 progress
208 end
204 end
209 end
205 end
210 end
206 end
General Comments 0
You need to be logged in to leave comments. Login now