##// END OF EJS Templates
Disallow users to delete a version referenced by a custom field (#20159)....
Jean-Philippe Lang -
r13994:bc9ca5fcfd74
parent child
Show More
@@ -1,182 +1,182
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 => [:index, :new, :create, :close_completed]
21 before_filter :find_model_object, :except => [:index, :new, :create, :close_completed]
22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
22 before_filter :find_project_from_association, :except => [:index, :new, :create, :close_completed]
23 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
23 before_filter :find_project_by_project_id, :only => [:index, :new, :create, :close_completed]
24 before_filter :authorize
24 before_filter :authorize
25
25
26 accept_api_auth :index, :show, :create, :update, :destroy
26 accept_api_auth :index, :show, :create, :update, :destroy
27
27
28 helper :custom_fields
28 helper :custom_fields
29 helper :projects
29 helper :projects
30
30
31 def index
31 def index
32 respond_to do |format|
32 respond_to do |format|
33 format.html {
33 format.html {
34 @trackers = @project.trackers.sorted.to_a
34 @trackers = @project.trackers.sorted.to_a
35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
35 retrieve_selected_tracker_ids(@trackers, @trackers.select {|t| t.is_in_roadmap?})
36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
36 @with_subprojects = params[:with_subprojects].nil? ? Setting.display_subprojects_issues? : (params[:with_subprojects] == '1')
37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
37 project_ids = @with_subprojects ? @project.self_and_descendants.collect(&:id) : [@project.id]
38
38
39 @versions = @project.shared_versions || []
39 @versions = @project.shared_versions || []
40 @versions += @project.rolled_up_versions.visible if @with_subprojects
40 @versions += @project.rolled_up_versions.visible if @with_subprojects
41 @versions = @versions.uniq.sort
41 @versions = @versions.uniq.sort
42 unless params[:completed]
42 unless params[:completed]
43 @completed_versions = @versions.select {|version| version.closed? || version.completed? }
43 @completed_versions = @versions.select {|version| version.closed? || version.completed? }
44 @versions -= @completed_versions
44 @versions -= @completed_versions
45 end
45 end
46
46
47 @issues_by_version = {}
47 @issues_by_version = {}
48 if @selected_tracker_ids.any? && @versions.any?
48 if @selected_tracker_ids.any? && @versions.any?
49 issues = Issue.visible.
49 issues = Issue.visible.
50 includes(:project, :tracker).
50 includes(:project, :tracker).
51 preload(:status, :priority, :fixed_version).
51 preload(:status, :priority, :fixed_version).
52 where(:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)).
52 where(:tracker_id => @selected_tracker_ids, :project_id => project_ids, :fixed_version_id => @versions.map(&:id)).
53 order("#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
53 order("#{Project.table_name}.lft, #{Tracker.table_name}.position, #{Issue.table_name}.id")
54 @issues_by_version = issues.group_by(&:fixed_version)
54 @issues_by_version = issues.group_by(&:fixed_version)
55 end
55 end
56 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
56 @versions.reject! {|version| !project_ids.include?(version.project_id) && @issues_by_version[version].blank?}
57 }
57 }
58 format.api {
58 format.api {
59 @versions = @project.shared_versions.to_a
59 @versions = @project.shared_versions.to_a
60 }
60 }
61 end
61 end
62 end
62 end
63
63
64 def show
64 def show
65 respond_to do |format|
65 respond_to do |format|
66 format.html {
66 format.html {
67 @issues = @version.fixed_issues.visible.
67 @issues = @version.fixed_issues.visible.
68 includes(:status, :tracker, :priority).
68 includes(:status, :tracker, :priority).
69 reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id").
69 reorder("#{Tracker.table_name}.position, #{Issue.table_name}.id").
70 to_a
70 to_a
71 }
71 }
72 format.api
72 format.api
73 end
73 end
74 end
74 end
75
75
76 def new
76 def new
77 @version = @project.versions.build
77 @version = @project.versions.build
78 @version.safe_attributes = params[:version]
78 @version.safe_attributes = params[:version]
79
79
80 respond_to do |format|
80 respond_to do |format|
81 format.html
81 format.html
82 format.js
82 format.js
83 end
83 end
84 end
84 end
85
85
86 def create
86 def create
87 @version = @project.versions.build
87 @version = @project.versions.build
88 if params[:version]
88 if params[:version]
89 attributes = params[:version].dup
89 attributes = params[:version].dup
90 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
90 attributes.delete('sharing') unless attributes.nil? || @version.allowed_sharings.include?(attributes['sharing'])
91 @version.safe_attributes = attributes
91 @version.safe_attributes = attributes
92 end
92 end
93
93
94 if request.post?
94 if request.post?
95 if @version.save
95 if @version.save
96 respond_to do |format|
96 respond_to do |format|
97 format.html do
97 format.html do
98 flash[:notice] = l(:notice_successful_create)
98 flash[:notice] = l(:notice_successful_create)
99 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
99 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
100 end
100 end
101 format.js
101 format.js
102 format.api do
102 format.api do
103 render :action => 'show', :status => :created, :location => version_url(@version)
103 render :action => 'show', :status => :created, :location => version_url(@version)
104 end
104 end
105 end
105 end
106 else
106 else
107 respond_to do |format|
107 respond_to do |format|
108 format.html { render :action => 'new' }
108 format.html { render :action => 'new' }
109 format.js { render :action => 'new' }
109 format.js { render :action => 'new' }
110 format.api { render_validation_errors(@version) }
110 format.api { render_validation_errors(@version) }
111 end
111 end
112 end
112 end
113 end
113 end
114 end
114 end
115
115
116 def edit
116 def edit
117 end
117 end
118
118
119 def update
119 def update
120 if params[:version]
120 if params[:version]
121 attributes = params[:version].dup
121 attributes = params[:version].dup
122 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
122 attributes.delete('sharing') unless @version.allowed_sharings.include?(attributes['sharing'])
123 @version.safe_attributes = attributes
123 @version.safe_attributes = attributes
124 if @version.save
124 if @version.save
125 respond_to do |format|
125 respond_to do |format|
126 format.html {
126 format.html {
127 flash[:notice] = l(:notice_successful_update)
127 flash[:notice] = l(:notice_successful_update)
128 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
128 redirect_back_or_default settings_project_path(@project, :tab => 'versions')
129 }
129 }
130 format.api { render_api_ok }
130 format.api { render_api_ok }
131 end
131 end
132 else
132 else
133 respond_to do |format|
133 respond_to do |format|
134 format.html { render :action => 'edit' }
134 format.html { render :action => 'edit' }
135 format.api { render_validation_errors(@version) }
135 format.api { render_validation_errors(@version) }
136 end
136 end
137 end
137 end
138 end
138 end
139 end
139 end
140
140
141 def close_completed
141 def close_completed
142 if request.put?
142 if request.put?
143 @project.close_completed_versions
143 @project.close_completed_versions
144 end
144 end
145 redirect_to settings_project_path(@project, :tab => 'versions')
145 redirect_to settings_project_path(@project, :tab => 'versions')
146 end
146 end
147
147
148 def destroy
148 def destroy
149 if @version.fixed_issues.empty?
149 if @version.deletable?
150 @version.destroy
150 @version.destroy
151 respond_to do |format|
151 respond_to do |format|
152 format.html { redirect_back_or_default settings_project_path(@project, :tab => 'versions') }
152 format.html { redirect_back_or_default settings_project_path(@project, :tab => 'versions') }
153 format.api { render_api_ok }
153 format.api { render_api_ok }
154 end
154 end
155 else
155 else
156 respond_to do |format|
156 respond_to do |format|
157 format.html {
157 format.html {
158 flash[:error] = l(:notice_unable_delete_version)
158 flash[:error] = l(:notice_unable_delete_version)
159 redirect_to settings_project_path(@project, :tab => 'versions')
159 redirect_to settings_project_path(@project, :tab => 'versions')
160 }
160 }
161 format.api { head :unprocessable_entity }
161 format.api { head :unprocessable_entity }
162 end
162 end
163 end
163 end
164 end
164 end
165
165
166 def status_by
166 def status_by
167 respond_to do |format|
167 respond_to do |format|
168 format.html { render :action => 'show' }
168 format.html { render :action => 'show' }
169 format.js
169 format.js
170 end
170 end
171 end
171 end
172
172
173 private
173 private
174
174
175 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
175 def retrieve_selected_tracker_ids(selectable_trackers, default_trackers=nil)
176 if ids = params[:tracker_ids]
176 if ids = params[:tracker_ids]
177 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
177 @selected_tracker_ids = (ids.is_a? Array) ? ids.collect { |id| id.to_i.to_s } : ids.split('/').collect { |id| id.to_i.to_s }
178 else
178 else
179 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
179 @selected_tracker_ids = (default_trackers || selectable_trackers).collect {|t| t.id.to_s }
180 end
180 end
181 end
181 end
182 end
182 end
@@ -1,291 +1,300
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 after_update :update_issues_from_sharing_change
20 after_update :update_issues_from_sharing_change
21 belongs_to :project
21 belongs_to :project
22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
22 has_many :fixed_issues, :class_name => 'Issue', :foreign_key => 'fixed_version_id', :dependent => :nullify
23 acts_as_customizable
23 acts_as_customizable
24 acts_as_attachable :view_permission => :view_files,
24 acts_as_attachable :view_permission => :view_files,
25 :edit_permission => :manage_files,
25 :edit_permission => :manage_files,
26 :delete_permission => :manage_files
26 :delete_permission => :manage_files
27
27
28 VERSION_STATUSES = %w(open locked closed)
28 VERSION_STATUSES = %w(open locked closed)
29 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
29 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
30
30
31 validates_presence_of :name
31 validates_presence_of :name
32 validates_uniqueness_of :name, :scope => [:project_id]
32 validates_uniqueness_of :name, :scope => [:project_id]
33 validates_length_of :name, :maximum => 60
33 validates_length_of :name, :maximum => 60
34 validates_length_of :description, :maximum => 255
34 validates_length_of :description, :maximum => 255
35 validates :effective_date, :date => true
35 validates :effective_date, :date => true
36 validates_inclusion_of :status, :in => VERSION_STATUSES
36 validates_inclusion_of :status, :in => VERSION_STATUSES
37 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
37 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
38 attr_protected :id
38 attr_protected :id
39
39
40 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
40 scope :named, lambda {|arg| where("LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip)}
41 scope :open, lambda { where(:status => 'open') }
41 scope :open, lambda { where(:status => 'open') }
42 scope :visible, lambda {|*args|
42 scope :visible, lambda {|*args|
43 joins(:project).
43 joins(:project).
44 where(Project.allowed_to_condition(args.first || User.current, :view_issues))
44 where(Project.allowed_to_condition(args.first || User.current, :view_issues))
45 }
45 }
46
46
47 safe_attributes 'name',
47 safe_attributes 'name',
48 'description',
48 'description',
49 'effective_date',
49 'effective_date',
50 'due_date',
50 'due_date',
51 'wiki_page_title',
51 'wiki_page_title',
52 'status',
52 'status',
53 'sharing',
53 'sharing',
54 'custom_field_values',
54 'custom_field_values',
55 'custom_fields'
55 'custom_fields'
56
56
57 # Returns true if +user+ or current user is allowed to view the version
57 # Returns true if +user+ or current user is allowed to view the version
58 def visible?(user=User.current)
58 def visible?(user=User.current)
59 user.allowed_to?(:view_issues, self.project)
59 user.allowed_to?(:view_issues, self.project)
60 end
60 end
61
61
62 # Version files have same visibility as project files
62 # Version files have same visibility as project files
63 def attachments_visible?(*args)
63 def attachments_visible?(*args)
64 project.present? && project.attachments_visible?(*args)
64 project.present? && project.attachments_visible?(*args)
65 end
65 end
66
66
67 def attachments_deletable?(usr=User.current)
67 def attachments_deletable?(usr=User.current)
68 project.present? && project.attachments_deletable?(usr)
68 project.present? && project.attachments_deletable?(usr)
69 end
69 end
70
70
71 def start_date
71 def start_date
72 @start_date ||= fixed_issues.minimum('start_date')
72 @start_date ||= fixed_issues.minimum('start_date')
73 end
73 end
74
74
75 def due_date
75 def due_date
76 effective_date
76 effective_date
77 end
77 end
78
78
79 def due_date=(arg)
79 def due_date=(arg)
80 self.effective_date=(arg)
80 self.effective_date=(arg)
81 end
81 end
82
82
83 # Returns the total estimated time for this version
83 # Returns the total estimated time for this version
84 # (sum of leaves estimated_hours)
84 # (sum of leaves estimated_hours)
85 def estimated_hours
85 def estimated_hours
86 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
86 @estimated_hours ||= fixed_issues.sum(:estimated_hours).to_f
87 end
87 end
88
88
89 # Returns the total reported time for this version
89 # Returns the total reported time for this version
90 def spent_hours
90 def spent_hours
91 @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
91 @spent_hours ||= TimeEntry.joins(:issue).where("#{Issue.table_name}.fixed_version_id = ?", id).sum(:hours).to_f
92 end
92 end
93
93
94 def closed?
94 def closed?
95 status == 'closed'
95 status == 'closed'
96 end
96 end
97
97
98 def open?
98 def open?
99 status == 'open'
99 status == 'open'
100 end
100 end
101
101
102 # Returns true if the version is completed: due date reached and no open issues
102 # Returns true if the version is completed: due date reached and no open issues
103 def completed?
103 def completed?
104 effective_date && (effective_date < Date.today) && (open_issues_count == 0)
104 effective_date && (effective_date < Date.today) && (open_issues_count == 0)
105 end
105 end
106
106
107 def behind_schedule?
107 def behind_schedule?
108 if completed_percent == 100
108 if completed_percent == 100
109 return false
109 return false
110 elsif due_date && start_date
110 elsif due_date && start_date
111 done_date = start_date + ((due_date - start_date+1)* completed_percent/100).floor
111 done_date = start_date + ((due_date - start_date+1)* completed_percent/100).floor
112 return done_date <= Date.today
112 return done_date <= Date.today
113 else
113 else
114 false # No issues so it's not late
114 false # No issues so it's not late
115 end
115 end
116 end
116 end
117
117
118 # Returns the completion percentage of this version based on the amount of open/closed issues
118 # Returns the completion percentage of this version based on the amount of open/closed issues
119 # and the time spent on the open issues.
119 # and the time spent on the open issues.
120 def completed_percent
120 def completed_percent
121 if issues_count == 0
121 if issues_count == 0
122 0
122 0
123 elsif open_issues_count == 0
123 elsif open_issues_count == 0
124 100
124 100
125 else
125 else
126 issues_progress(false) + issues_progress(true)
126 issues_progress(false) + issues_progress(true)
127 end
127 end
128 end
128 end
129
129
130 # Returns the percentage of issues that have been marked as 'closed'.
130 # Returns the percentage of issues that have been marked as 'closed'.
131 def closed_percent
131 def closed_percent
132 if issues_count == 0
132 if issues_count == 0
133 0
133 0
134 else
134 else
135 issues_progress(false)
135 issues_progress(false)
136 end
136 end
137 end
137 end
138
138
139 # Returns true if the version is overdue: due date reached and some open issues
139 # Returns true if the version is overdue: due date reached and some open issues
140 def overdue?
140 def overdue?
141 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
141 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
142 end
142 end
143
143
144 # Returns assigned issues count
144 # Returns assigned issues count
145 def issues_count
145 def issues_count
146 load_issue_counts
146 load_issue_counts
147 @issue_count
147 @issue_count
148 end
148 end
149
149
150 # Returns the total amount of open issues for this version.
150 # Returns the total amount of open issues for this version.
151 def open_issues_count
151 def open_issues_count
152 load_issue_counts
152 load_issue_counts
153 @open_issues_count
153 @open_issues_count
154 end
154 end
155
155
156 # Returns the total amount of closed issues for this version.
156 # Returns the total amount of closed issues for this version.
157 def closed_issues_count
157 def closed_issues_count
158 load_issue_counts
158 load_issue_counts
159 @closed_issues_count
159 @closed_issues_count
160 end
160 end
161
161
162 def wiki_page
162 def wiki_page
163 if project.wiki && !wiki_page_title.blank?
163 if project.wiki && !wiki_page_title.blank?
164 @wiki_page ||= project.wiki.find_page(wiki_page_title)
164 @wiki_page ||= project.wiki.find_page(wiki_page_title)
165 end
165 end
166 @wiki_page
166 @wiki_page
167 end
167 end
168
168
169 def to_s; name end
169 def to_s; name end
170
170
171 def to_s_with_project
171 def to_s_with_project
172 "#{project} - #{name}"
172 "#{project} - #{name}"
173 end
173 end
174
174
175 # Versions are sorted by effective_date and name
175 # Versions are sorted by effective_date and name
176 # Those with no effective_date are at the end, sorted by name
176 # Those with no effective_date are at the end, sorted by name
177 def <=>(version)
177 def <=>(version)
178 if self.effective_date
178 if self.effective_date
179 if version.effective_date
179 if version.effective_date
180 if self.effective_date == version.effective_date
180 if self.effective_date == version.effective_date
181 name == version.name ? id <=> version.id : name <=> version.name
181 name == version.name ? id <=> version.id : name <=> version.name
182 else
182 else
183 self.effective_date <=> version.effective_date
183 self.effective_date <=> version.effective_date
184 end
184 end
185 else
185 else
186 -1
186 -1
187 end
187 end
188 else
188 else
189 if version.effective_date
189 if version.effective_date
190 1
190 1
191 else
191 else
192 name == version.name ? id <=> version.id : name <=> version.name
192 name == version.name ? id <=> version.id : name <=> version.name
193 end
193 end
194 end
194 end
195 end
195 end
196
196
197 def self.fields_for_order_statement(table=nil)
197 def self.fields_for_order_statement(table=nil)
198 table ||= table_name
198 table ||= table_name
199 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
199 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
200 end
200 end
201
201
202 scope :sorted, lambda { order(fields_for_order_statement) }
202 scope :sorted, lambda { order(fields_for_order_statement) }
203
203
204 # Returns the sharings that +user+ can set the version to
204 # Returns the sharings that +user+ can set the version to
205 def allowed_sharings(user = User.current)
205 def allowed_sharings(user = User.current)
206 VERSION_SHARINGS.select do |s|
206 VERSION_SHARINGS.select do |s|
207 if sharing == s
207 if sharing == s
208 true
208 true
209 else
209 else
210 case s
210 case s
211 when 'system'
211 when 'system'
212 # Only admin users can set a systemwide sharing
212 # Only admin users can set a systemwide sharing
213 user.admin?
213 user.admin?
214 when 'hierarchy', 'tree'
214 when 'hierarchy', 'tree'
215 # Only users allowed to manage versions of the root project can
215 # Only users allowed to manage versions of the root project can
216 # set sharing to hierarchy or tree
216 # set sharing to hierarchy or tree
217 project.nil? || user.allowed_to?(:manage_versions, project.root)
217 project.nil? || user.allowed_to?(:manage_versions, project.root)
218 else
218 else
219 true
219 true
220 end
220 end
221 end
221 end
222 end
222 end
223 end
223 end
224
224
225 # Returns true if the version is shared, otherwise false
225 # Returns true if the version is shared, otherwise false
226 def shared?
226 def shared?
227 sharing != 'none'
227 sharing != 'none'
228 end
228 end
229
229
230 def deletable?
231 fixed_issues.empty? && !referenced_by_a_custom_field?
232 end
233
230 private
234 private
231
235
232 def load_issue_counts
236 def load_issue_counts
233 unless @issue_count
237 unless @issue_count
234 @open_issues_count = 0
238 @open_issues_count = 0
235 @closed_issues_count = 0
239 @closed_issues_count = 0
236 fixed_issues.group(:status).count.each do |status, count|
240 fixed_issues.group(:status).count.each do |status, count|
237 if status.is_closed?
241 if status.is_closed?
238 @closed_issues_count += count
242 @closed_issues_count += count
239 else
243 else
240 @open_issues_count += count
244 @open_issues_count += count
241 end
245 end
242 end
246 end
243 @issue_count = @open_issues_count + @closed_issues_count
247 @issue_count = @open_issues_count + @closed_issues_count
244 end
248 end
245 end
249 end
246
250
247 # Update the issue's fixed versions. Used if a version's sharing changes.
251 # Update the issue's fixed versions. Used if a version's sharing changes.
248 def update_issues_from_sharing_change
252 def update_issues_from_sharing_change
249 if sharing_changed?
253 if sharing_changed?
250 if VERSION_SHARINGS.index(sharing_was).nil? ||
254 if VERSION_SHARINGS.index(sharing_was).nil? ||
251 VERSION_SHARINGS.index(sharing).nil? ||
255 VERSION_SHARINGS.index(sharing).nil? ||
252 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
256 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
253 Issue.update_versions_from_sharing_change self
257 Issue.update_versions_from_sharing_change self
254 end
258 end
255 end
259 end
256 end
260 end
257
261
258 # Returns the average estimated time of assigned issues
262 # Returns the average estimated time of assigned issues
259 # or 1 if no issue has an estimated time
263 # or 1 if no issue has an estimated time
260 # Used to weight unestimated issues in progress calculation
264 # Used to weight unestimated issues in progress calculation
261 def estimated_average
265 def estimated_average
262 if @estimated_average.nil?
266 if @estimated_average.nil?
263 average = fixed_issues.average(:estimated_hours).to_f
267 average = fixed_issues.average(:estimated_hours).to_f
264 if average == 0
268 if average == 0
265 average = 1
269 average = 1
266 end
270 end
267 @estimated_average = average
271 @estimated_average = average
268 end
272 end
269 @estimated_average
273 @estimated_average
270 end
274 end
271
275
272 # Returns the total progress of open or closed issues. The returned percentage takes into account
276 # Returns the total progress of open or closed issues. The returned percentage takes into account
273 # the amount of estimated time set for this version.
277 # the amount of estimated time set for this version.
274 #
278 #
275 # Examples:
279 # Examples:
276 # issues_progress(true) => returns the progress percentage for open issues.
280 # issues_progress(true) => returns the progress percentage for open issues.
277 # issues_progress(false) => returns the progress percentage for closed issues.
281 # issues_progress(false) => returns the progress percentage for closed issues.
278 def issues_progress(open)
282 def issues_progress(open)
279 @issues_progress ||= {}
283 @issues_progress ||= {}
280 @issues_progress[open] ||= begin
284 @issues_progress[open] ||= begin
281 progress = 0
285 progress = 0
282 if issues_count > 0
286 if issues_count > 0
283 ratio = open ? 'done_ratio' : 100
287 ratio = open ? 'done_ratio' : 100
284
288
285 done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
289 done = fixed_issues.open(open).sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}").to_f
286 progress = done / (estimated_average * issues_count)
290 progress = done / (estimated_average * issues_count)
287 end
291 end
288 progress
292 progress
289 end
293 end
290 end
294 end
295
296 def referenced_by_a_custom_field?
297 CustomValue.joins(:custom_field).
298 where(:value => id.to_s, :custom_fields => {:field_format => 'version'}).any?
299 end
291 end
300 end
@@ -1,241 +1,242
1 module ObjectHelpers
1 module ObjectHelpers
2 def User.generate!(attributes={})
2 def User.generate!(attributes={})
3 @generated_user_login ||= 'user0'
3 @generated_user_login ||= 'user0'
4 @generated_user_login.succ!
4 @generated_user_login.succ!
5 user = User.new(attributes)
5 user = User.new(attributes)
6 user.login = @generated_user_login.dup if user.login.blank?
6 user.login = @generated_user_login.dup if user.login.blank?
7 user.mail = "#{@generated_user_login}@example.com" if user.mail.blank?
7 user.mail = "#{@generated_user_login}@example.com" if user.mail.blank?
8 user.firstname = "Bob" if user.firstname.blank?
8 user.firstname = "Bob" if user.firstname.blank?
9 user.lastname = "Doe" if user.lastname.blank?
9 user.lastname = "Doe" if user.lastname.blank?
10 yield user if block_given?
10 yield user if block_given?
11 user.save!
11 user.save!
12 user
12 user
13 end
13 end
14
14
15 def User.add_to_project(user, project, roles=nil)
15 def User.add_to_project(user, project, roles=nil)
16 roles = Role.find(1) if roles.nil?
16 roles = Role.find(1) if roles.nil?
17 roles = [roles] if roles.is_a?(Role)
17 roles = [roles] if roles.is_a?(Role)
18 Member.create!(:principal => user, :project => project, :roles => roles)
18 Member.create!(:principal => user, :project => project, :roles => roles)
19 end
19 end
20
20
21 def Group.generate!(attributes={})
21 def Group.generate!(attributes={})
22 @generated_group_name ||= 'Group 0'
22 @generated_group_name ||= 'Group 0'
23 @generated_group_name.succ!
23 @generated_group_name.succ!
24 group = Group.new(attributes)
24 group = Group.new(attributes)
25 group.name = @generated_group_name.dup if group.name.blank?
25 group.name = @generated_group_name.dup if group.name.blank?
26 yield group if block_given?
26 yield group if block_given?
27 group.save!
27 group.save!
28 group
28 group
29 end
29 end
30
30
31 def Project.generate!(attributes={})
31 def Project.generate!(attributes={})
32 @generated_project_identifier ||= 'project-0000'
32 @generated_project_identifier ||= 'project-0000'
33 @generated_project_identifier.succ!
33 @generated_project_identifier.succ!
34 project = Project.new(attributes)
34 project = Project.new(attributes)
35 project.name = @generated_project_identifier.dup if project.name.blank?
35 project.name = @generated_project_identifier.dup if project.name.blank?
36 project.identifier = @generated_project_identifier.dup if project.identifier.blank?
36 project.identifier = @generated_project_identifier.dup if project.identifier.blank?
37 yield project if block_given?
37 yield project if block_given?
38 project.save!
38 project.save!
39 project
39 project
40 end
40 end
41
41
42 def Project.generate_with_parent!(parent, attributes={})
42 def Project.generate_with_parent!(parent, attributes={})
43 project = Project.generate!(attributes) do |p|
43 project = Project.generate!(attributes) do |p|
44 p.parent = parent
44 p.parent = parent
45 end
45 end
46 parent.reload if parent
46 parent.reload if parent
47 project
47 project
48 end
48 end
49
49
50 def IssueStatus.generate!(attributes={})
50 def IssueStatus.generate!(attributes={})
51 @generated_status_name ||= 'Status 0'
51 @generated_status_name ||= 'Status 0'
52 @generated_status_name.succ!
52 @generated_status_name.succ!
53 status = IssueStatus.new(attributes)
53 status = IssueStatus.new(attributes)
54 status.name = @generated_status_name.dup if status.name.blank?
54 status.name = @generated_status_name.dup if status.name.blank?
55 yield status if block_given?
55 yield status if block_given?
56 status.save!
56 status.save!
57 status
57 status
58 end
58 end
59
59
60 def Tracker.generate!(attributes={})
60 def Tracker.generate!(attributes={})
61 @generated_tracker_name ||= 'Tracker 0'
61 @generated_tracker_name ||= 'Tracker 0'
62 @generated_tracker_name.succ!
62 @generated_tracker_name.succ!
63 tracker = Tracker.new(attributes)
63 tracker = Tracker.new(attributes)
64 tracker.name = @generated_tracker_name.dup if tracker.name.blank?
64 tracker.name = @generated_tracker_name.dup if tracker.name.blank?
65 tracker.default_status ||= IssueStatus.order('position').first || IssueStatus.generate!
65 tracker.default_status ||= IssueStatus.order('position').first || IssueStatus.generate!
66 yield tracker if block_given?
66 yield tracker if block_given?
67 tracker.save!
67 tracker.save!
68 tracker
68 tracker
69 end
69 end
70
70
71 def Role.generate!(attributes={})
71 def Role.generate!(attributes={})
72 @generated_role_name ||= 'Role 0'
72 @generated_role_name ||= 'Role 0'
73 @generated_role_name.succ!
73 @generated_role_name.succ!
74 role = Role.new(attributes)
74 role = Role.new(attributes)
75 role.name = @generated_role_name.dup if role.name.blank?
75 role.name = @generated_role_name.dup if role.name.blank?
76 yield role if block_given?
76 yield role if block_given?
77 role.save!
77 role.save!
78 role
78 role
79 end
79 end
80
80
81 # Generates an unsaved Issue
81 # Generates an unsaved Issue
82 def Issue.generate(attributes={})
82 def Issue.generate(attributes={})
83 issue = Issue.new(attributes)
83 issue = Issue.new(attributes)
84 issue.project ||= Project.find(1)
84 issue.project ||= Project.find(1)
85 issue.tracker ||= issue.project.trackers.first
85 issue.tracker ||= issue.project.trackers.first
86 issue.subject = 'Generated' if issue.subject.blank?
86 issue.subject = 'Generated' if issue.subject.blank?
87 issue.author ||= User.find(2)
87 issue.author ||= User.find(2)
88 yield issue if block_given?
88 yield issue if block_given?
89 issue
89 issue
90 end
90 end
91
91
92 # Generates a saved Issue
92 # Generates a saved Issue
93 def Issue.generate!(attributes={}, &block)
93 def Issue.generate!(attributes={}, &block)
94 issue = Issue.generate(attributes, &block)
94 issue = Issue.generate(attributes, &block)
95 issue.save!
95 issue.save!
96 issue
96 issue
97 end
97 end
98
98
99 # Generates an issue with 2 children and a grandchild
99 # Generates an issue with 2 children and a grandchild
100 def Issue.generate_with_descendants!(attributes={})
100 def Issue.generate_with_descendants!(attributes={})
101 issue = Issue.generate!(attributes)
101 issue = Issue.generate!(attributes)
102 child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id)
102 child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id)
103 Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id)
103 Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id)
104 Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id)
104 Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id)
105 issue.reload
105 issue.reload
106 end
106 end
107
107
108 def Issue.generate_with_child!(attributes={})
108 def Issue.generate_with_child!(attributes={})
109 issue = Issue.generate!(attributes)
109 issue = Issue.generate!(attributes)
110 Issue.generate!(:parent_issue_id => issue.id)
110 Issue.generate!(:parent_issue_id => issue.id)
111 issue.reload
111 issue.reload
112 end
112 end
113
113
114 def Journal.generate!(attributes={})
114 def Journal.generate!(attributes={})
115 journal = Journal.new(attributes)
115 journal = Journal.new(attributes)
116 journal.user ||= User.first
116 journal.user ||= User.first
117 journal.journalized ||= Issue.first
117 journal.journalized ||= Issue.first
118 yield journal if block_given?
118 yield journal if block_given?
119 journal.save!
119 journal.save!
120 journal
120 journal
121 end
121 end
122
122
123 def Version.generate!(attributes={})
123 def Version.generate!(attributes={})
124 @generated_version_name ||= 'Version 0'
124 @generated_version_name ||= 'Version 0'
125 @generated_version_name.succ!
125 @generated_version_name.succ!
126 version = Version.new(attributes)
126 version = Version.new(attributes)
127 version.name = @generated_version_name.dup if version.name.blank?
127 version.name = @generated_version_name.dup if version.name.blank?
128 version.project ||= Project.find(1)
128 yield version if block_given?
129 yield version if block_given?
129 version.save!
130 version.save!
130 version
131 version
131 end
132 end
132
133
133 def TimeEntry.generate!(attributes={})
134 def TimeEntry.generate!(attributes={})
134 entry = TimeEntry.new(attributes)
135 entry = TimeEntry.new(attributes)
135 entry.user ||= User.find(2)
136 entry.user ||= User.find(2)
136 entry.issue ||= Issue.find(1) unless entry.project
137 entry.issue ||= Issue.find(1) unless entry.project
137 entry.project ||= entry.issue.project
138 entry.project ||= entry.issue.project
138 entry.activity ||= TimeEntryActivity.first
139 entry.activity ||= TimeEntryActivity.first
139 entry.spent_on ||= Date.today
140 entry.spent_on ||= Date.today
140 entry.hours ||= 1.0
141 entry.hours ||= 1.0
141 entry.save!
142 entry.save!
142 entry
143 entry
143 end
144 end
144
145
145 def AuthSource.generate!(attributes={})
146 def AuthSource.generate!(attributes={})
146 @generated_auth_source_name ||= 'Auth 0'
147 @generated_auth_source_name ||= 'Auth 0'
147 @generated_auth_source_name.succ!
148 @generated_auth_source_name.succ!
148 source = AuthSource.new(attributes)
149 source = AuthSource.new(attributes)
149 source.name = @generated_auth_source_name.dup if source.name.blank?
150 source.name = @generated_auth_source_name.dup if source.name.blank?
150 yield source if block_given?
151 yield source if block_given?
151 source.save!
152 source.save!
152 source
153 source
153 end
154 end
154
155
155 def Board.generate!(attributes={})
156 def Board.generate!(attributes={})
156 @generated_board_name ||= 'Forum 0'
157 @generated_board_name ||= 'Forum 0'
157 @generated_board_name.succ!
158 @generated_board_name.succ!
158 board = Board.new(attributes)
159 board = Board.new(attributes)
159 board.name = @generated_board_name.dup if board.name.blank?
160 board.name = @generated_board_name.dup if board.name.blank?
160 board.description = @generated_board_name.dup if board.description.blank?
161 board.description = @generated_board_name.dup if board.description.blank?
161 yield board if block_given?
162 yield board if block_given?
162 board.save!
163 board.save!
163 board
164 board
164 end
165 end
165
166
166 def Attachment.generate!(attributes={})
167 def Attachment.generate!(attributes={})
167 @generated_filename ||= 'testfile0'
168 @generated_filename ||= 'testfile0'
168 @generated_filename.succ!
169 @generated_filename.succ!
169 attributes = attributes.dup
170 attributes = attributes.dup
170 attachment = Attachment.new(attributes)
171 attachment = Attachment.new(attributes)
171 attachment.container ||= Issue.find(1)
172 attachment.container ||= Issue.find(1)
172 attachment.author ||= User.find(2)
173 attachment.author ||= User.find(2)
173 attachment.filename = @generated_filename.dup if attachment.filename.blank?
174 attachment.filename = @generated_filename.dup if attachment.filename.blank?
174 attachment.save!
175 attachment.save!
175 attachment
176 attachment
176 end
177 end
177
178
178 def CustomField.generate!(attributes={})
179 def CustomField.generate!(attributes={})
179 @generated_custom_field_name ||= 'Custom field 0'
180 @generated_custom_field_name ||= 'Custom field 0'
180 @generated_custom_field_name.succ!
181 @generated_custom_field_name.succ!
181 field = new(attributes)
182 field = new(attributes)
182 field.name = @generated_custom_field_name.dup if field.name.blank?
183 field.name = @generated_custom_field_name.dup if field.name.blank?
183 field.field_format = 'string' if field.field_format.blank?
184 field.field_format = 'string' if field.field_format.blank?
184 yield field if block_given?
185 yield field if block_given?
185 field.save!
186 field.save!
186 field
187 field
187 end
188 end
188
189
189 def Changeset.generate!(attributes={})
190 def Changeset.generate!(attributes={})
190 @generated_changeset_rev ||= '123456'
191 @generated_changeset_rev ||= '123456'
191 @generated_changeset_rev.succ!
192 @generated_changeset_rev.succ!
192 changeset = new(attributes)
193 changeset = new(attributes)
193 changeset.repository ||= Project.find(1).repository
194 changeset.repository ||= Project.find(1).repository
194 changeset.revision ||= @generated_changeset_rev
195 changeset.revision ||= @generated_changeset_rev
195 changeset.committed_on ||= Time.now
196 changeset.committed_on ||= Time.now
196 yield changeset if block_given?
197 yield changeset if block_given?
197 changeset.save!
198 changeset.save!
198 changeset
199 changeset
199 end
200 end
200
201
201 def Query.generate!(attributes={})
202 def Query.generate!(attributes={})
202 query = new(attributes)
203 query = new(attributes)
203 query.name = "Generated query" if query.name.blank?
204 query.name = "Generated query" if query.name.blank?
204 query.user ||= User.find(1)
205 query.user ||= User.find(1)
205 query.save!
206 query.save!
206 query
207 query
207 end
208 end
208 end
209 end
209
210
210 module TrackerObjectHelpers
211 module TrackerObjectHelpers
211 def generate_transitions!(*args)
212 def generate_transitions!(*args)
212 options = args.last.is_a?(Hash) ? args.pop : {}
213 options = args.last.is_a?(Hash) ? args.pop : {}
213 if args.size == 1
214 if args.size == 1
214 args << args.first
215 args << args.first
215 end
216 end
216 if options[:clear]
217 if options[:clear]
217 WorkflowTransition.where(:tracker_id => id).delete_all
218 WorkflowTransition.where(:tracker_id => id).delete_all
218 end
219 end
219 args.each_cons(2) do |old_status_id, new_status_id|
220 args.each_cons(2) do |old_status_id, new_status_id|
220 WorkflowTransition.create!(
221 WorkflowTransition.create!(
221 :tracker => self,
222 :tracker => self,
222 :role_id => (options[:role_id] || 1),
223 :role_id => (options[:role_id] || 1),
223 :old_status_id => old_status_id,
224 :old_status_id => old_status_id,
224 :new_status_id => new_status_id
225 :new_status_id => new_status_id
225 )
226 )
226 end
227 end
227 end
228 end
228 end
229 end
229 Tracker.send :include, TrackerObjectHelpers
230 Tracker.send :include, TrackerObjectHelpers
230
231
231 module IssueObjectHelpers
232 module IssueObjectHelpers
232 def close!
233 def close!
233 self.status = IssueStatus.where(:is_closed => true).first
234 self.status = IssueStatus.where(:is_closed => true).first
234 save!
235 save!
235 end
236 end
236
237
237 def generate_child!(attributes={})
238 def generate_child!(attributes={})
238 Issue.generate!(attributes.merge(:parent_issue_id => self.id))
239 Issue.generate!(attributes.merge(:parent_issue_id => self.id))
239 end
240 end
240 end
241 end
241 Issue.send :include, IssueObjectHelpers
242 Issue.send :include, IssueObjectHelpers
@@ -1,251 +1,272
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class VersionTest < ActiveSupport::TestCase
20 class VersionTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :issues, :issue_statuses, :trackers,
21 fixtures :projects, :users, :issues, :issue_statuses, :trackers,
22 :enumerations, :versions, :projects_trackers
22 :enumerations, :versions, :projects_trackers
23
23
24 def test_create
24 def test_create
25 v = Version.new(:project => Project.find(1), :name => '1.1',
25 v = Version.new(:project => Project.find(1), :name => '1.1',
26 :effective_date => '2011-03-25')
26 :effective_date => '2011-03-25')
27 assert v.save
27 assert v.save
28 assert_equal 'open', v.status
28 assert_equal 'open', v.status
29 assert_equal 'none', v.sharing
29 assert_equal 'none', v.sharing
30 end
30 end
31
31
32 def test_invalid_effective_date_validation
32 def test_invalid_effective_date_validation
33 v = Version.new(:project => Project.find(1), :name => '1.1',
33 v = Version.new(:project => Project.find(1), :name => '1.1',
34 :effective_date => '99999-01-01')
34 :effective_date => '99999-01-01')
35 assert !v.valid?
35 assert !v.valid?
36 v.effective_date = '2012-11-33'
36 v.effective_date = '2012-11-33'
37 assert !v.valid?
37 assert !v.valid?
38 v.effective_date = '2012-31-11'
38 v.effective_date = '2012-31-11'
39 assert !v.valid?
39 assert !v.valid?
40 v.effective_date = '-2012-31-11'
40 v.effective_date = '-2012-31-11'
41 assert !v.valid?
41 assert !v.valid?
42 v.effective_date = 'ABC'
42 v.effective_date = 'ABC'
43 assert !v.valid?
43 assert !v.valid?
44 assert_include I18n.translate('activerecord.errors.messages.not_a_date'),
44 assert_include I18n.translate('activerecord.errors.messages.not_a_date'),
45 v.errors[:effective_date]
45 v.errors[:effective_date]
46 end
46 end
47
47
48 def test_progress_should_be_0_with_no_assigned_issues
48 def test_progress_should_be_0_with_no_assigned_issues
49 project = Project.find(1)
49 project = Project.find(1)
50 v = Version.create!(:project => project, :name => 'Progress')
50 v = Version.create!(:project => project, :name => 'Progress')
51 assert_equal 0, v.completed_percent
51 assert_equal 0, v.completed_percent
52 assert_equal 0, v.closed_percent
52 assert_equal 0, v.closed_percent
53 end
53 end
54
54
55 def test_progress_should_be_0_with_unbegun_assigned_issues
55 def test_progress_should_be_0_with_unbegun_assigned_issues
56 project = Project.find(1)
56 project = Project.find(1)
57 v = Version.create!(:project => project, :name => 'Progress')
57 v = Version.create!(:project => project, :name => 'Progress')
58 add_issue(v)
58 add_issue(v)
59 add_issue(v, :done_ratio => 0)
59 add_issue(v, :done_ratio => 0)
60 assert_progress_equal 0, v.completed_percent
60 assert_progress_equal 0, v.completed_percent
61 assert_progress_equal 0, v.closed_percent
61 assert_progress_equal 0, v.closed_percent
62 end
62 end
63
63
64 def test_progress_should_be_100_with_closed_assigned_issues
64 def test_progress_should_be_100_with_closed_assigned_issues
65 project = Project.find(1)
65 project = Project.find(1)
66 status = IssueStatus.where(:is_closed => true).first
66 status = IssueStatus.where(:is_closed => true).first
67 v = Version.create!(:project => project, :name => 'Progress')
67 v = Version.create!(:project => project, :name => 'Progress')
68 add_issue(v, :status => status)
68 add_issue(v, :status => status)
69 add_issue(v, :status => status, :done_ratio => 20)
69 add_issue(v, :status => status, :done_ratio => 20)
70 add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25)
70 add_issue(v, :status => status, :done_ratio => 70, :estimated_hours => 25)
71 add_issue(v, :status => status, :estimated_hours => 15)
71 add_issue(v, :status => status, :estimated_hours => 15)
72 assert_progress_equal 100.0, v.completed_percent
72 assert_progress_equal 100.0, v.completed_percent
73 assert_progress_equal 100.0, v.closed_percent
73 assert_progress_equal 100.0, v.closed_percent
74 end
74 end
75
75
76 def test_progress_should_consider_done_ratio_of_open_assigned_issues
76 def test_progress_should_consider_done_ratio_of_open_assigned_issues
77 project = Project.find(1)
77 project = Project.find(1)
78 v = Version.create!(:project => project, :name => 'Progress')
78 v = Version.create!(:project => project, :name => 'Progress')
79 add_issue(v)
79 add_issue(v)
80 add_issue(v, :done_ratio => 20)
80 add_issue(v, :done_ratio => 20)
81 add_issue(v, :done_ratio => 70)
81 add_issue(v, :done_ratio => 70)
82 assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_percent
82 assert_progress_equal (0.0 + 20.0 + 70.0)/3, v.completed_percent
83 assert_progress_equal 0, v.closed_percent
83 assert_progress_equal 0, v.closed_percent
84 end
84 end
85
85
86 def test_progress_should_consider_closed_issues_as_completed
86 def test_progress_should_consider_closed_issues_as_completed
87 project = Project.find(1)
87 project = Project.find(1)
88 v = Version.create!(:project => project, :name => 'Progress')
88 v = Version.create!(:project => project, :name => 'Progress')
89 add_issue(v)
89 add_issue(v)
90 add_issue(v, :done_ratio => 20)
90 add_issue(v, :done_ratio => 20)
91 add_issue(v, :status => IssueStatus.where(:is_closed => true).first)
91 add_issue(v, :status => IssueStatus.where(:is_closed => true).first)
92 assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_percent
92 assert_progress_equal (0.0 + 20.0 + 100.0)/3, v.completed_percent
93 assert_progress_equal (100.0)/3, v.closed_percent
93 assert_progress_equal (100.0)/3, v.closed_percent
94 end
94 end
95
95
96 def test_progress_should_consider_estimated_hours_to_weight_issues
96 def test_progress_should_consider_estimated_hours_to_weight_issues
97 project = Project.find(1)
97 project = Project.find(1)
98 v = Version.create!(:project => project, :name => 'Progress')
98 v = Version.create!(:project => project, :name => 'Progress')
99 add_issue(v, :estimated_hours => 10)
99 add_issue(v, :estimated_hours => 10)
100 add_issue(v, :estimated_hours => 20, :done_ratio => 30)
100 add_issue(v, :estimated_hours => 20, :done_ratio => 30)
101 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
101 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
102 add_issue(v, :estimated_hours => 25, :status => IssueStatus.where(:is_closed => true).first)
102 add_issue(v, :estimated_hours => 25, :status => IssueStatus.where(:is_closed => true).first)
103 assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_percent
103 assert_progress_equal (10.0*0 + 20.0*0.3 + 40*0.1 + 25.0*1)/95.0*100, v.completed_percent
104 assert_progress_equal 25.0/95.0*100, v.closed_percent
104 assert_progress_equal 25.0/95.0*100, v.closed_percent
105 end
105 end
106
106
107 def test_progress_should_consider_average_estimated_hours_to_weight_unestimated_issues
107 def test_progress_should_consider_average_estimated_hours_to_weight_unestimated_issues
108 project = Project.find(1)
108 project = Project.find(1)
109 v = Version.create!(:project => project, :name => 'Progress')
109 v = Version.create!(:project => project, :name => 'Progress')
110 add_issue(v, :done_ratio => 20)
110 add_issue(v, :done_ratio => 20)
111 add_issue(v, :status => IssueStatus.where(:is_closed => true).first)
111 add_issue(v, :status => IssueStatus.where(:is_closed => true).first)
112 add_issue(v, :estimated_hours => 10, :done_ratio => 30)
112 add_issue(v, :estimated_hours => 10, :done_ratio => 30)
113 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
113 add_issue(v, :estimated_hours => 40, :done_ratio => 10)
114 assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_percent
114 assert_progress_equal (25.0*0.2 + 25.0*1 + 10.0*0.3 + 40.0*0.1)/100.0*100, v.completed_percent
115 assert_progress_equal 25.0/100.0*100, v.closed_percent
115 assert_progress_equal 25.0/100.0*100, v.closed_percent
116 end
116 end
117
117
118 def test_should_sort_scheduled_then_unscheduled_versions
118 def test_should_sort_scheduled_then_unscheduled_versions
119 Version.delete_all
119 Version.delete_all
120 v4 = Version.create!(:project_id => 1, :name => 'v4')
120 v4 = Version.create!(:project_id => 1, :name => 'v4')
121 v3 = Version.create!(:project_id => 1, :name => 'v2', :effective_date => '2012-07-14')
121 v3 = Version.create!(:project_id => 1, :name => 'v2', :effective_date => '2012-07-14')
122 v2 = Version.create!(:project_id => 1, :name => 'v1')
122 v2 = Version.create!(:project_id => 1, :name => 'v1')
123 v1 = Version.create!(:project_id => 1, :name => 'v3', :effective_date => '2012-08-02')
123 v1 = Version.create!(:project_id => 1, :name => 'v3', :effective_date => '2012-08-02')
124 v5 = Version.create!(:project_id => 1, :name => 'v5', :effective_date => '2012-07-02')
124 v5 = Version.create!(:project_id => 1, :name => 'v5', :effective_date => '2012-07-02')
125
125
126 assert_equal [v5, v3, v1, v2, v4], [v1, v2, v3, v4, v5].sort
126 assert_equal [v5, v3, v1, v2, v4], [v1, v2, v3, v4, v5].sort
127 assert_equal [v5, v3, v1, v2, v4], Version.sorted.to_a
127 assert_equal [v5, v3, v1, v2, v4], Version.sorted.to_a
128 end
128 end
129
129
130 def test_should_sort_versions_with_same_date_by_name
130 def test_should_sort_versions_with_same_date_by_name
131 v1 = Version.new(:effective_date => '2014-12-03', :name => 'v2')
131 v1 = Version.new(:effective_date => '2014-12-03', :name => 'v2')
132 v2 = Version.new(:effective_date => '2014-12-03', :name => 'v1')
132 v2 = Version.new(:effective_date => '2014-12-03', :name => 'v1')
133 assert_equal [v2, v1], [v1, v2].sort
133 assert_equal [v2, v1], [v1, v2].sort
134 end
134 end
135
135
136 def test_completed_should_be_false_when_due_today
136 def test_completed_should_be_false_when_due_today
137 version = Version.create!(:project_id => 1, :effective_date => Date.today, :name => 'Due today')
137 version = Version.create!(:project_id => 1, :effective_date => Date.today, :name => 'Due today')
138 assert_equal false, version.completed?
138 assert_equal false, version.completed?
139 end
139 end
140
140
141 test "#behind_schedule? should be false if there are no issues assigned" do
141 test "#behind_schedule? should be false if there are no issues assigned" do
142 version = Version.generate!(:effective_date => Date.yesterday)
142 version = Version.generate!(:effective_date => Date.yesterday)
143 assert_equal false, version.behind_schedule?
143 assert_equal false, version.behind_schedule?
144 end
144 end
145
145
146 test "#behind_schedule? should be false if there is no effective_date" do
146 test "#behind_schedule? should be false if there is no effective_date" do
147 version = Version.generate!(:effective_date => nil)
147 version = Version.generate!(:effective_date => nil)
148 assert_equal false, version.behind_schedule?
148 assert_equal false, version.behind_schedule?
149 end
149 end
150
150
151 test "#behind_schedule? should be false if all of the issues are ahead of schedule" do
151 test "#behind_schedule? should be false if all of the issues are ahead of schedule" do
152 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
152 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
153 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
153 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
154 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
154 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
155 assert_equal 60, version.completed_percent
155 assert_equal 60, version.completed_percent
156 assert_equal false, version.behind_schedule?
156 assert_equal false, version.behind_schedule?
157 end
157 end
158
158
159 test "#behind_schedule? should be true if any of the issues are behind schedule" do
159 test "#behind_schedule? should be true if any of the issues are behind schedule" do
160 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
160 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
161 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
161 add_issue(version, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
162 add_issue(version, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left
162 add_issue(version, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left
163 assert_equal 40, version.completed_percent
163 assert_equal 40, version.completed_percent
164 assert_equal true, version.behind_schedule?
164 assert_equal true, version.behind_schedule?
165 end
165 end
166
166
167 test "#behind_schedule? should be false if all of the issues are complete" do
167 test "#behind_schedule? should be false if all of the issues are complete" do
168 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
168 version = Version.create!(:project_id => 1, :name => 'test', :effective_date => 7.days.from_now.to_date)
169 add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
169 add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
170 add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
170 add_issue(version, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
171 assert_equal 100, version.completed_percent
171 assert_equal 100, version.completed_percent
172 assert_equal false, version.behind_schedule?
172 assert_equal false, version.behind_schedule?
173 end
173 end
174
174
175 test "#estimated_hours should return 0 with no assigned issues" do
175 test "#estimated_hours should return 0 with no assigned issues" do
176 version = Version.generate!
176 version = Version.generate!
177 assert_equal 0, version.estimated_hours
177 assert_equal 0, version.estimated_hours
178 end
178 end
179
179
180 test "#estimated_hours should return 0 with no estimated hours" do
180 test "#estimated_hours should return 0 with no estimated hours" do
181 version = Version.create!(:project_id => 1, :name => 'test')
181 version = Version.create!(:project_id => 1, :name => 'test')
182 add_issue(version)
182 add_issue(version)
183 assert_equal 0, version.estimated_hours
183 assert_equal 0, version.estimated_hours
184 end
184 end
185
185
186 test "#estimated_hours should return return the sum of estimated hours" do
186 test "#estimated_hours should return return the sum of estimated hours" do
187 version = Version.create!(:project_id => 1, :name => 'test')
187 version = Version.create!(:project_id => 1, :name => 'test')
188 add_issue(version, :estimated_hours => 2.5)
188 add_issue(version, :estimated_hours => 2.5)
189 add_issue(version, :estimated_hours => 5)
189 add_issue(version, :estimated_hours => 5)
190 assert_equal 7.5, version.estimated_hours
190 assert_equal 7.5, version.estimated_hours
191 end
191 end
192
192
193 test "#estimated_hours should return the sum of leaves estimated hours" do
193 test "#estimated_hours should return the sum of leaves estimated hours" do
194 version = Version.create!(:project_id => 1, :name => 'test')
194 version = Version.create!(:project_id => 1, :name => 'test')
195 parent = add_issue(version)
195 parent = add_issue(version)
196 add_issue(version, :estimated_hours => 2.5, :parent_issue_id => parent.id)
196 add_issue(version, :estimated_hours => 2.5, :parent_issue_id => parent.id)
197 add_issue(version, :estimated_hours => 5, :parent_issue_id => parent.id)
197 add_issue(version, :estimated_hours => 5, :parent_issue_id => parent.id)
198 assert_equal 7.5, version.estimated_hours
198 assert_equal 7.5, version.estimated_hours
199 end
199 end
200
200
201 test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do
201 test "should update all issue's fixed_version associations in case the hierarchy changed XXX" do
202 User.current = User.find(1) # Need the admin's permissions
202 User.current = User.find(1) # Need the admin's permissions
203
203
204 @version = Version.find(7)
204 @version = Version.find(7)
205 # Separate hierarchy
205 # Separate hierarchy
206 project_1_issue = Issue.find(1)
206 project_1_issue = Issue.find(1)
207 project_1_issue.fixed_version = @version
207 project_1_issue.fixed_version = @version
208 assert project_1_issue.save, project_1_issue.errors.full_messages.to_s
208 assert project_1_issue.save, project_1_issue.errors.full_messages.to_s
209
209
210 project_5_issue = Issue.find(6)
210 project_5_issue = Issue.find(6)
211 project_5_issue.fixed_version = @version
211 project_5_issue.fixed_version = @version
212 assert project_5_issue.save
212 assert project_5_issue.save
213
213
214 # Project
214 # Project
215 project_2_issue = Issue.find(4)
215 project_2_issue = Issue.find(4)
216 project_2_issue.fixed_version = @version
216 project_2_issue.fixed_version = @version
217 assert project_2_issue.save
217 assert project_2_issue.save
218
218
219 # Update the sharing
219 # Update the sharing
220 @version.sharing = 'none'
220 @version.sharing = 'none'
221 assert @version.save
221 assert @version.save
222
222
223 # Project 1 now out of the shared scope
223 # Project 1 now out of the shared scope
224 project_1_issue.reload
224 project_1_issue.reload
225 assert_equal nil, project_1_issue.fixed_version,
225 assert_equal nil, project_1_issue.fixed_version,
226 "Fixed version is still set after changing the Version's sharing"
226 "Fixed version is still set after changing the Version's sharing"
227
227
228 # Project 5 now out of the shared scope
228 # Project 5 now out of the shared scope
229 project_5_issue.reload
229 project_5_issue.reload
230 assert_equal nil, project_5_issue.fixed_version,
230 assert_equal nil, project_5_issue.fixed_version,
231 "Fixed version is still set after changing the Version's sharing"
231 "Fixed version is still set after changing the Version's sharing"
232
232
233 # Project 2 issue remains
233 # Project 2 issue remains
234 project_2_issue.reload
234 project_2_issue.reload
235 assert_equal @version, project_2_issue.fixed_version
235 assert_equal @version, project_2_issue.fixed_version
236 end
236 end
237
237
238 def test_deletable_should_return_true_when_not_referenced
239 version = Version.generate!
240
241 assert_equal true, version.deletable?
242 end
243
244 def test_deletable_should_return_false_when_referenced_by_an_issue
245 version = Version.generate!
246 Issue.generate!(:fixed_version => version)
247
248 assert_equal false, version.deletable?
249 end
250
251 def test_deletable_should_return_false_when_referenced_by_a_custom_field
252 version = Version.generate!
253 field = IssueCustomField.generate!(:field_format => 'version')
254 value = CustomValue.create!(:custom_field => field, :customized => Issue.first, :value => version.id)
255
256 assert_equal false, version.deletable?
257 end
258
238 private
259 private
239
260
240 def add_issue(version, attributes={})
261 def add_issue(version, attributes={})
241 Issue.create!({:project => version.project,
262 Issue.create!({:project => version.project,
242 :fixed_version => version,
263 :fixed_version => version,
243 :subject => 'Test',
264 :subject => 'Test',
244 :author => User.first,
265 :author => User.first,
245 :tracker => version.project.trackers.first}.merge(attributes))
266 :tracker => version.project.trackers.first}.merge(attributes))
246 end
267 end
247
268
248 def assert_progress_equal(expected_float, actual_float, message="")
269 def assert_progress_equal(expected_float, actual_float, message="")
249 assert_in_delta(expected_float, actual_float, 0.000001, message="")
270 assert_in_delta(expected_float, actual_float, 0.000001, message="")
250 end
271 end
251 end
272 end
General Comments 0
You need to be logged in to leave comments. Login now