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