##// END OF EJS Templates
Fixed that open scope on Project#issues raises an error (#11545)....
Jean-Philippe Lang -
r10016:8fb1a7e3ccc7
parent child
Show More
@@ -1,1250 +1,1249
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue, :validate_required_fields
61 validate :validate_issue, :validate_required_fields
62
62
63 scope :visible,
63 scope :visible,
64 lambda {|*args| { :include => :project,
64 lambda {|*args| { :include => :project,
65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
66
66
67 class << self; undef :open; end
68 scope :open, lambda {|*args|
67 scope :open, lambda {|*args|
69 is_closed = args.size > 0 ? !args.first : false
68 is_closed = args.size > 0 ? !args.first : false
70 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
71 }
70 }
72
71
73 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
74 scope :with_limit, lambda { |limit| { :limit => limit} }
73 scope :with_limit, lambda { |limit| { :limit => limit} }
75 scope :on_active_project, :include => [:status, :project, :tracker],
74 scope :on_active_project, :include => [:status, :project, :tracker],
76 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
77
76
78 before_create :default_assign
77 before_create :default_assign
79 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
78 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
80 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
81 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
82 after_destroy :update_parent_attributes
81 after_destroy :update_parent_attributes
83
82
84 # Returns a SQL conditions string used to find all issues visible by the specified user
83 # Returns a SQL conditions string used to find all issues visible by the specified user
85 def self.visible_condition(user, options={})
84 def self.visible_condition(user, options={})
86 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
85 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
87 case role.issues_visibility
86 case role.issues_visibility
88 when 'all'
87 when 'all'
89 nil
88 nil
90 when 'default'
89 when 'default'
91 user_ids = [user.id] + user.groups.map(&:id)
90 user_ids = [user.id] + user.groups.map(&:id)
92 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
91 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 when 'own'
92 when 'own'
94 user_ids = [user.id] + user.groups.map(&:id)
93 user_ids = [user.id] + user.groups.map(&:id)
95 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
96 else
95 else
97 '1=0'
96 '1=0'
98 end
97 end
99 end
98 end
100 end
99 end
101
100
102 # Returns true if usr or current user is allowed to view the issue
101 # Returns true if usr or current user is allowed to view the issue
103 def visible?(usr=nil)
102 def visible?(usr=nil)
104 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
103 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
105 case role.issues_visibility
104 case role.issues_visibility
106 when 'all'
105 when 'all'
107 true
106 true
108 when 'default'
107 when 'default'
109 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
108 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
110 when 'own'
109 when 'own'
111 self.author == user || user.is_or_belongs_to?(assigned_to)
110 self.author == user || user.is_or_belongs_to?(assigned_to)
112 else
111 else
113 false
112 false
114 end
113 end
115 end
114 end
116 end
115 end
117
116
118 def initialize(attributes=nil, *args)
117 def initialize(attributes=nil, *args)
119 super
118 super
120 if new_record?
119 if new_record?
121 # set default values for new records only
120 # set default values for new records only
122 self.status ||= IssueStatus.default
121 self.status ||= IssueStatus.default
123 self.priority ||= IssuePriority.default
122 self.priority ||= IssuePriority.default
124 self.watcher_user_ids = []
123 self.watcher_user_ids = []
125 end
124 end
126 end
125 end
127
126
128 # AR#Persistence#destroy would raise and RecordNotFound exception
127 # AR#Persistence#destroy would raise and RecordNotFound exception
129 # if the issue was already deleted or updated (non matching lock_version).
128 # if the issue was already deleted or updated (non matching lock_version).
130 # This is a problem when bulk deleting issues or deleting a project
129 # This is a problem when bulk deleting issues or deleting a project
131 # (because an issue may already be deleted if its parent was deleted
130 # (because an issue may already be deleted if its parent was deleted
132 # first).
131 # first).
133 # The issue is reloaded by the nested_set before being deleted so
132 # The issue is reloaded by the nested_set before being deleted so
134 # the lock_version condition should not be an issue but we handle it.
133 # the lock_version condition should not be an issue but we handle it.
135 def destroy
134 def destroy
136 super
135 super
137 rescue ActiveRecord::RecordNotFound
136 rescue ActiveRecord::RecordNotFound
138 # Stale or already deleted
137 # Stale or already deleted
139 begin
138 begin
140 reload
139 reload
141 rescue ActiveRecord::RecordNotFound
140 rescue ActiveRecord::RecordNotFound
142 # The issue was actually already deleted
141 # The issue was actually already deleted
143 @destroyed = true
142 @destroyed = true
144 return freeze
143 return freeze
145 end
144 end
146 # The issue was stale, retry to destroy
145 # The issue was stale, retry to destroy
147 super
146 super
148 end
147 end
149
148
150 def reload(*args)
149 def reload(*args)
151 @workflow_rule_by_attribute = nil
150 @workflow_rule_by_attribute = nil
152 @assignable_versions = nil
151 @assignable_versions = nil
153 super
152 super
154 end
153 end
155
154
156 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
155 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
157 def available_custom_fields
156 def available_custom_fields
158 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
157 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
159 end
158 end
160
159
161 # Copies attributes from another issue, arg can be an id or an Issue
160 # Copies attributes from another issue, arg can be an id or an Issue
162 def copy_from(arg, options={})
161 def copy_from(arg, options={})
163 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
162 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
164 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
163 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
165 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
164 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
166 self.status = issue.status
165 self.status = issue.status
167 self.author = User.current
166 self.author = User.current
168 unless options[:attachments] == false
167 unless options[:attachments] == false
169 self.attachments = issue.attachments.map do |attachement|
168 self.attachments = issue.attachments.map do |attachement|
170 attachement.copy(:container => self)
169 attachement.copy(:container => self)
171 end
170 end
172 end
171 end
173 @copied_from = issue
172 @copied_from = issue
174 self
173 self
175 end
174 end
176
175
177 # Returns an unsaved copy of the issue
176 # Returns an unsaved copy of the issue
178 def copy(attributes=nil, copy_options={})
177 def copy(attributes=nil, copy_options={})
179 copy = self.class.new.copy_from(self, copy_options)
178 copy = self.class.new.copy_from(self, copy_options)
180 copy.attributes = attributes if attributes
179 copy.attributes = attributes if attributes
181 copy
180 copy
182 end
181 end
183
182
184 # Returns true if the issue is a copy
183 # Returns true if the issue is a copy
185 def copy?
184 def copy?
186 @copied_from.present?
185 @copied_from.present?
187 end
186 end
188
187
189 # Moves/copies an issue to a new project and tracker
188 # Moves/copies an issue to a new project and tracker
190 # Returns the moved/copied issue on success, false on failure
189 # Returns the moved/copied issue on success, false on failure
191 def move_to_project(new_project, new_tracker=nil, options={})
190 def move_to_project(new_project, new_tracker=nil, options={})
192 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
191 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
193
192
194 if options[:copy]
193 if options[:copy]
195 issue = self.copy
194 issue = self.copy
196 else
195 else
197 issue = self
196 issue = self
198 end
197 end
199
198
200 issue.init_journal(User.current, options[:notes])
199 issue.init_journal(User.current, options[:notes])
201
200
202 # Preserve previous behaviour
201 # Preserve previous behaviour
203 # #move_to_project doesn't change tracker automatically
202 # #move_to_project doesn't change tracker automatically
204 issue.send :project=, new_project, true
203 issue.send :project=, new_project, true
205 if new_tracker
204 if new_tracker
206 issue.tracker = new_tracker
205 issue.tracker = new_tracker
207 end
206 end
208 # Allow bulk setting of attributes on the issue
207 # Allow bulk setting of attributes on the issue
209 if options[:attributes]
208 if options[:attributes]
210 issue.attributes = options[:attributes]
209 issue.attributes = options[:attributes]
211 end
210 end
212
211
213 issue.save ? issue : false
212 issue.save ? issue : false
214 end
213 end
215
214
216 def status_id=(sid)
215 def status_id=(sid)
217 self.status = nil
216 self.status = nil
218 result = write_attribute(:status_id, sid)
217 result = write_attribute(:status_id, sid)
219 @workflow_rule_by_attribute = nil
218 @workflow_rule_by_attribute = nil
220 result
219 result
221 end
220 end
222
221
223 def priority_id=(pid)
222 def priority_id=(pid)
224 self.priority = nil
223 self.priority = nil
225 write_attribute(:priority_id, pid)
224 write_attribute(:priority_id, pid)
226 end
225 end
227
226
228 def category_id=(cid)
227 def category_id=(cid)
229 self.category = nil
228 self.category = nil
230 write_attribute(:category_id, cid)
229 write_attribute(:category_id, cid)
231 end
230 end
232
231
233 def fixed_version_id=(vid)
232 def fixed_version_id=(vid)
234 self.fixed_version = nil
233 self.fixed_version = nil
235 write_attribute(:fixed_version_id, vid)
234 write_attribute(:fixed_version_id, vid)
236 end
235 end
237
236
238 def tracker_id=(tid)
237 def tracker_id=(tid)
239 self.tracker = nil
238 self.tracker = nil
240 result = write_attribute(:tracker_id, tid)
239 result = write_attribute(:tracker_id, tid)
241 @custom_field_values = nil
240 @custom_field_values = nil
242 @workflow_rule_by_attribute = nil
241 @workflow_rule_by_attribute = nil
243 result
242 result
244 end
243 end
245
244
246 def project_id=(project_id)
245 def project_id=(project_id)
247 if project_id.to_s != self.project_id.to_s
246 if project_id.to_s != self.project_id.to_s
248 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
247 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
249 end
248 end
250 end
249 end
251
250
252 def project=(project, keep_tracker=false)
251 def project=(project, keep_tracker=false)
253 project_was = self.project
252 project_was = self.project
254 write_attribute(:project_id, project ? project.id : nil)
253 write_attribute(:project_id, project ? project.id : nil)
255 association_instance_set('project', project)
254 association_instance_set('project', project)
256 if project_was && project && project_was != project
255 if project_was && project && project_was != project
257 @assignable_versions = nil
256 @assignable_versions = nil
258
257
259 unless keep_tracker || project.trackers.include?(tracker)
258 unless keep_tracker || project.trackers.include?(tracker)
260 self.tracker = project.trackers.first
259 self.tracker = project.trackers.first
261 end
260 end
262 # Reassign to the category with same name if any
261 # Reassign to the category with same name if any
263 if category
262 if category
264 self.category = project.issue_categories.find_by_name(category.name)
263 self.category = project.issue_categories.find_by_name(category.name)
265 end
264 end
266 # Keep the fixed_version if it's still valid in the new_project
265 # Keep the fixed_version if it's still valid in the new_project
267 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
266 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
268 self.fixed_version = nil
267 self.fixed_version = nil
269 end
268 end
270 if parent && parent.project_id != project_id
269 if parent && parent.project_id != project_id
271 self.parent_issue_id = nil
270 self.parent_issue_id = nil
272 end
271 end
273 @custom_field_values = nil
272 @custom_field_values = nil
274 end
273 end
275 end
274 end
276
275
277 def description=(arg)
276 def description=(arg)
278 if arg.is_a?(String)
277 if arg.is_a?(String)
279 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
278 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
280 end
279 end
281 write_attribute(:description, arg)
280 write_attribute(:description, arg)
282 end
281 end
283
282
284 # Overrides assign_attributes so that project and tracker get assigned first
283 # Overrides assign_attributes so that project and tracker get assigned first
285 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
284 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
286 return if new_attributes.nil?
285 return if new_attributes.nil?
287 attrs = new_attributes.dup
286 attrs = new_attributes.dup
288 attrs.stringify_keys!
287 attrs.stringify_keys!
289
288
290 %w(project project_id tracker tracker_id).each do |attr|
289 %w(project project_id tracker tracker_id).each do |attr|
291 if attrs.has_key?(attr)
290 if attrs.has_key?(attr)
292 send "#{attr}=", attrs.delete(attr)
291 send "#{attr}=", attrs.delete(attr)
293 end
292 end
294 end
293 end
295 send :assign_attributes_without_project_and_tracker_first, attrs, *args
294 send :assign_attributes_without_project_and_tracker_first, attrs, *args
296 end
295 end
297 # Do not redefine alias chain on reload (see #4838)
296 # Do not redefine alias chain on reload (see #4838)
298 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
297 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
299
298
300 def estimated_hours=(h)
299 def estimated_hours=(h)
301 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
300 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
302 end
301 end
303
302
304 safe_attributes 'project_id',
303 safe_attributes 'project_id',
305 :if => lambda {|issue, user|
304 :if => lambda {|issue, user|
306 if issue.new_record?
305 if issue.new_record?
307 issue.copy?
306 issue.copy?
308 elsif user.allowed_to?(:move_issues, issue.project)
307 elsif user.allowed_to?(:move_issues, issue.project)
309 projects = Issue.allowed_target_projects_on_move(user)
308 projects = Issue.allowed_target_projects_on_move(user)
310 projects.include?(issue.project) && projects.size > 1
309 projects.include?(issue.project) && projects.size > 1
311 end
310 end
312 }
311 }
313
312
314 safe_attributes 'tracker_id',
313 safe_attributes 'tracker_id',
315 'status_id',
314 'status_id',
316 'category_id',
315 'category_id',
317 'assigned_to_id',
316 'assigned_to_id',
318 'priority_id',
317 'priority_id',
319 'fixed_version_id',
318 'fixed_version_id',
320 'subject',
319 'subject',
321 'description',
320 'description',
322 'start_date',
321 'start_date',
323 'due_date',
322 'due_date',
324 'done_ratio',
323 'done_ratio',
325 'estimated_hours',
324 'estimated_hours',
326 'custom_field_values',
325 'custom_field_values',
327 'custom_fields',
326 'custom_fields',
328 'lock_version',
327 'lock_version',
329 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
328 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
330
329
331 safe_attributes 'status_id',
330 safe_attributes 'status_id',
332 'assigned_to_id',
331 'assigned_to_id',
333 'fixed_version_id',
332 'fixed_version_id',
334 'done_ratio',
333 'done_ratio',
335 'lock_version',
334 'lock_version',
336 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
335 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
337
336
338 safe_attributes 'watcher_user_ids',
337 safe_attributes 'watcher_user_ids',
339 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
338 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
340
339
341 safe_attributes 'is_private',
340 safe_attributes 'is_private',
342 :if => lambda {|issue, user|
341 :if => lambda {|issue, user|
343 user.allowed_to?(:set_issues_private, issue.project) ||
342 user.allowed_to?(:set_issues_private, issue.project) ||
344 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
343 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
345 }
344 }
346
345
347 safe_attributes 'parent_issue_id',
346 safe_attributes 'parent_issue_id',
348 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
347 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
349 user.allowed_to?(:manage_subtasks, issue.project)}
348 user.allowed_to?(:manage_subtasks, issue.project)}
350
349
351 def safe_attribute_names(user=nil)
350 def safe_attribute_names(user=nil)
352 names = super
351 names = super
353 names -= disabled_core_fields
352 names -= disabled_core_fields
354 names -= read_only_attribute_names(user)
353 names -= read_only_attribute_names(user)
355 names
354 names
356 end
355 end
357
356
358 # Safely sets attributes
357 # Safely sets attributes
359 # Should be called from controllers instead of #attributes=
358 # Should be called from controllers instead of #attributes=
360 # attr_accessible is too rough because we still want things like
359 # attr_accessible is too rough because we still want things like
361 # Issue.new(:project => foo) to work
360 # Issue.new(:project => foo) to work
362 def safe_attributes=(attrs, user=User.current)
361 def safe_attributes=(attrs, user=User.current)
363 return unless attrs.is_a?(Hash)
362 return unless attrs.is_a?(Hash)
364
363
365 attrs = attrs.dup
364 attrs = attrs.dup
366
365
367 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
366 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
368 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
367 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
369 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
368 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
370 self.project_id = p
369 self.project_id = p
371 end
370 end
372 end
371 end
373
372
374 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
373 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
375 self.tracker_id = t
374 self.tracker_id = t
376 end
375 end
377
376
378 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
377 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
379 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
378 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
380 self.status_id = s
379 self.status_id = s
381 end
380 end
382 end
381 end
383
382
384 attrs = delete_unsafe_attributes(attrs, user)
383 attrs = delete_unsafe_attributes(attrs, user)
385 return if attrs.empty?
384 return if attrs.empty?
386
385
387 unless leaf?
386 unless leaf?
388 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
387 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
389 end
388 end
390
389
391 if attrs['parent_issue_id'].present?
390 if attrs['parent_issue_id'].present?
392 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
391 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
393 end
392 end
394
393
395 if attrs['custom_field_values'].present?
394 if attrs['custom_field_values'].present?
396 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
395 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
397 end
396 end
398
397
399 if attrs['custom_fields'].present?
398 if attrs['custom_fields'].present?
400 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
399 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
401 end
400 end
402
401
403 # mass-assignment security bypass
402 # mass-assignment security bypass
404 assign_attributes attrs, :without_protection => true
403 assign_attributes attrs, :without_protection => true
405 end
404 end
406
405
407 def disabled_core_fields
406 def disabled_core_fields
408 tracker ? tracker.disabled_core_fields : []
407 tracker ? tracker.disabled_core_fields : []
409 end
408 end
410
409
411 # Returns the custom_field_values that can be edited by the given user
410 # Returns the custom_field_values that can be edited by the given user
412 def editable_custom_field_values(user=nil)
411 def editable_custom_field_values(user=nil)
413 custom_field_values.reject do |value|
412 custom_field_values.reject do |value|
414 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
413 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
415 end
414 end
416 end
415 end
417
416
418 # Returns the names of attributes that are read-only for user or the current user
417 # Returns the names of attributes that are read-only for user or the current user
419 # For users with multiple roles, the read-only fields are the intersection of
418 # For users with multiple roles, the read-only fields are the intersection of
420 # read-only fields of each role
419 # read-only fields of each role
421 # The result is an array of strings where sustom fields are represented with their ids
420 # The result is an array of strings where sustom fields are represented with their ids
422 #
421 #
423 # Examples:
422 # Examples:
424 # issue.read_only_attribute_names # => ['due_date', '2']
423 # issue.read_only_attribute_names # => ['due_date', '2']
425 # issue.read_only_attribute_names(user) # => []
424 # issue.read_only_attribute_names(user) # => []
426 def read_only_attribute_names(user=nil)
425 def read_only_attribute_names(user=nil)
427 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
426 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
428 end
427 end
429
428
430 # Returns the names of required attributes for user or the current user
429 # Returns the names of required attributes for user or the current user
431 # For users with multiple roles, the required fields are the intersection of
430 # For users with multiple roles, the required fields are the intersection of
432 # required fields of each role
431 # required fields of each role
433 # The result is an array of strings where sustom fields are represented with their ids
432 # The result is an array of strings where sustom fields are represented with their ids
434 #
433 #
435 # Examples:
434 # Examples:
436 # issue.required_attribute_names # => ['due_date', '2']
435 # issue.required_attribute_names # => ['due_date', '2']
437 # issue.required_attribute_names(user) # => []
436 # issue.required_attribute_names(user) # => []
438 def required_attribute_names(user=nil)
437 def required_attribute_names(user=nil)
439 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
438 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
440 end
439 end
441
440
442 # Returns true if the attribute is required for user
441 # Returns true if the attribute is required for user
443 def required_attribute?(name, user=nil)
442 def required_attribute?(name, user=nil)
444 required_attribute_names(user).include?(name.to_s)
443 required_attribute_names(user).include?(name.to_s)
445 end
444 end
446
445
447 # Returns a hash of the workflow rule by attribute for the given user
446 # Returns a hash of the workflow rule by attribute for the given user
448 #
447 #
449 # Examples:
448 # Examples:
450 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
449 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
451 def workflow_rule_by_attribute(user=nil)
450 def workflow_rule_by_attribute(user=nil)
452 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
451 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
453
452
454 user_real = user || User.current
453 user_real = user || User.current
455 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
454 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
456 return {} if roles.empty?
455 return {} if roles.empty?
457
456
458 result = {}
457 result = {}
459 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
458 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
460 if workflow_permissions.any?
459 if workflow_permissions.any?
461 workflow_rules = workflow_permissions.inject({}) do |h, wp|
460 workflow_rules = workflow_permissions.inject({}) do |h, wp|
462 h[wp.field_name] ||= []
461 h[wp.field_name] ||= []
463 h[wp.field_name] << wp.rule
462 h[wp.field_name] << wp.rule
464 h
463 h
465 end
464 end
466 workflow_rules.each do |attr, rules|
465 workflow_rules.each do |attr, rules|
467 next if rules.size < roles.size
466 next if rules.size < roles.size
468 uniq_rules = rules.uniq
467 uniq_rules = rules.uniq
469 if uniq_rules.size == 1
468 if uniq_rules.size == 1
470 result[attr] = uniq_rules.first
469 result[attr] = uniq_rules.first
471 else
470 else
472 result[attr] = 'required'
471 result[attr] = 'required'
473 end
472 end
474 end
473 end
475 end
474 end
476 @workflow_rule_by_attribute = result if user.nil?
475 @workflow_rule_by_attribute = result if user.nil?
477 result
476 result
478 end
477 end
479 private :workflow_rule_by_attribute
478 private :workflow_rule_by_attribute
480
479
481 def done_ratio
480 def done_ratio
482 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
481 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
483 status.default_done_ratio
482 status.default_done_ratio
484 else
483 else
485 read_attribute(:done_ratio)
484 read_attribute(:done_ratio)
486 end
485 end
487 end
486 end
488
487
489 def self.use_status_for_done_ratio?
488 def self.use_status_for_done_ratio?
490 Setting.issue_done_ratio == 'issue_status'
489 Setting.issue_done_ratio == 'issue_status'
491 end
490 end
492
491
493 def self.use_field_for_done_ratio?
492 def self.use_field_for_done_ratio?
494 Setting.issue_done_ratio == 'issue_field'
493 Setting.issue_done_ratio == 'issue_field'
495 end
494 end
496
495
497 def validate_issue
496 def validate_issue
498 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
497 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
499 errors.add :due_date, :not_a_date
498 errors.add :due_date, :not_a_date
500 end
499 end
501
500
502 if self.due_date and self.start_date and self.due_date < self.start_date
501 if self.due_date and self.start_date and self.due_date < self.start_date
503 errors.add :due_date, :greater_than_start_date
502 errors.add :due_date, :greater_than_start_date
504 end
503 end
505
504
506 if start_date && soonest_start && start_date < soonest_start
505 if start_date && soonest_start && start_date < soonest_start
507 errors.add :start_date, :invalid
506 errors.add :start_date, :invalid
508 end
507 end
509
508
510 if fixed_version
509 if fixed_version
511 if !assignable_versions.include?(fixed_version)
510 if !assignable_versions.include?(fixed_version)
512 errors.add :fixed_version_id, :inclusion
511 errors.add :fixed_version_id, :inclusion
513 elsif reopened? && fixed_version.closed?
512 elsif reopened? && fixed_version.closed?
514 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
513 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
515 end
514 end
516 end
515 end
517
516
518 # Checks that the issue can not be added/moved to a disabled tracker
517 # Checks that the issue can not be added/moved to a disabled tracker
519 if project && (tracker_id_changed? || project_id_changed?)
518 if project && (tracker_id_changed? || project_id_changed?)
520 unless project.trackers.include?(tracker)
519 unless project.trackers.include?(tracker)
521 errors.add :tracker_id, :inclusion
520 errors.add :tracker_id, :inclusion
522 end
521 end
523 end
522 end
524
523
525 # Checks parent issue assignment
524 # Checks parent issue assignment
526 if @parent_issue
525 if @parent_issue
527 if @parent_issue.project_id != project_id
526 if @parent_issue.project_id != project_id
528 errors.add :parent_issue_id, :not_same_project
527 errors.add :parent_issue_id, :not_same_project
529 elsif !new_record?
528 elsif !new_record?
530 # moving an existing issue
529 # moving an existing issue
531 if @parent_issue.root_id != root_id
530 if @parent_issue.root_id != root_id
532 # we can always move to another tree
531 # we can always move to another tree
533 elsif move_possible?(@parent_issue)
532 elsif move_possible?(@parent_issue)
534 # move accepted inside tree
533 # move accepted inside tree
535 else
534 else
536 errors.add :parent_issue_id, :not_a_valid_parent
535 errors.add :parent_issue_id, :not_a_valid_parent
537 end
536 end
538 end
537 end
539 end
538 end
540 end
539 end
541
540
542 # Validates the issue against additional workflow requirements
541 # Validates the issue against additional workflow requirements
543 def validate_required_fields
542 def validate_required_fields
544 user = new_record? ? author : current_journal.try(:user)
543 user = new_record? ? author : current_journal.try(:user)
545
544
546 required_attribute_names(user).each do |attribute|
545 required_attribute_names(user).each do |attribute|
547 if attribute =~ /^\d+$/
546 if attribute =~ /^\d+$/
548 attribute = attribute.to_i
547 attribute = attribute.to_i
549 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
548 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
550 if v && v.value.blank?
549 if v && v.value.blank?
551 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
550 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
552 end
551 end
553 else
552 else
554 if respond_to?(attribute) && send(attribute).blank?
553 if respond_to?(attribute) && send(attribute).blank?
555 errors.add attribute, :blank
554 errors.add attribute, :blank
556 end
555 end
557 end
556 end
558 end
557 end
559 end
558 end
560
559
561 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
560 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
562 # even if the user turns off the setting later
561 # even if the user turns off the setting later
563 def update_done_ratio_from_issue_status
562 def update_done_ratio_from_issue_status
564 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
563 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
565 self.done_ratio = status.default_done_ratio
564 self.done_ratio = status.default_done_ratio
566 end
565 end
567 end
566 end
568
567
569 def init_journal(user, notes = "")
568 def init_journal(user, notes = "")
570 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
569 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
571 if new_record?
570 if new_record?
572 @current_journal.notify = false
571 @current_journal.notify = false
573 else
572 else
574 @attributes_before_change = attributes.dup
573 @attributes_before_change = attributes.dup
575 @custom_values_before_change = {}
574 @custom_values_before_change = {}
576 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
575 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
577 end
576 end
578 @current_journal
577 @current_journal
579 end
578 end
580
579
581 # Returns the id of the last journal or nil
580 # Returns the id of the last journal or nil
582 def last_journal_id
581 def last_journal_id
583 if new_record?
582 if new_record?
584 nil
583 nil
585 else
584 else
586 journals.maximum(:id)
585 journals.maximum(:id)
587 end
586 end
588 end
587 end
589
588
590 # Returns a scope for journals that have an id greater than journal_id
589 # Returns a scope for journals that have an id greater than journal_id
591 def journals_after(journal_id)
590 def journals_after(journal_id)
592 scope = journals.reorder("#{Journal.table_name}.id ASC")
591 scope = journals.reorder("#{Journal.table_name}.id ASC")
593 if journal_id.present?
592 if journal_id.present?
594 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
593 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
595 end
594 end
596 scope
595 scope
597 end
596 end
598
597
599 # Return true if the issue is closed, otherwise false
598 # Return true if the issue is closed, otherwise false
600 def closed?
599 def closed?
601 self.status.is_closed?
600 self.status.is_closed?
602 end
601 end
603
602
604 # Return true if the issue is being reopened
603 # Return true if the issue is being reopened
605 def reopened?
604 def reopened?
606 if !new_record? && status_id_changed?
605 if !new_record? && status_id_changed?
607 status_was = IssueStatus.find_by_id(status_id_was)
606 status_was = IssueStatus.find_by_id(status_id_was)
608 status_new = IssueStatus.find_by_id(status_id)
607 status_new = IssueStatus.find_by_id(status_id)
609 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
608 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
610 return true
609 return true
611 end
610 end
612 end
611 end
613 false
612 false
614 end
613 end
615
614
616 # Return true if the issue is being closed
615 # Return true if the issue is being closed
617 def closing?
616 def closing?
618 if !new_record? && status_id_changed?
617 if !new_record? && status_id_changed?
619 status_was = IssueStatus.find_by_id(status_id_was)
618 status_was = IssueStatus.find_by_id(status_id_was)
620 status_new = IssueStatus.find_by_id(status_id)
619 status_new = IssueStatus.find_by_id(status_id)
621 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
620 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
622 return true
621 return true
623 end
622 end
624 end
623 end
625 false
624 false
626 end
625 end
627
626
628 # Returns true if the issue is overdue
627 # Returns true if the issue is overdue
629 def overdue?
628 def overdue?
630 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
629 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
631 end
630 end
632
631
633 # Is the amount of work done less than it should for the due date
632 # Is the amount of work done less than it should for the due date
634 def behind_schedule?
633 def behind_schedule?
635 return false if start_date.nil? || due_date.nil?
634 return false if start_date.nil? || due_date.nil?
636 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
635 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
637 return done_date <= Date.today
636 return done_date <= Date.today
638 end
637 end
639
638
640 # Does this issue have children?
639 # Does this issue have children?
641 def children?
640 def children?
642 !leaf?
641 !leaf?
643 end
642 end
644
643
645 # Users the issue can be assigned to
644 # Users the issue can be assigned to
646 def assignable_users
645 def assignable_users
647 users = project.assignable_users
646 users = project.assignable_users
648 users << author if author
647 users << author if author
649 users << assigned_to if assigned_to
648 users << assigned_to if assigned_to
650 users.uniq.sort
649 users.uniq.sort
651 end
650 end
652
651
653 # Versions that the issue can be assigned to
652 # Versions that the issue can be assigned to
654 def assignable_versions
653 def assignable_versions
655 return @assignable_versions if @assignable_versions
654 return @assignable_versions if @assignable_versions
656
655
657 versions = project.shared_versions.open.all
656 versions = project.shared_versions.open.all
658 if fixed_version
657 if fixed_version
659 if fixed_version_id_changed?
658 if fixed_version_id_changed?
660 # nothing to do
659 # nothing to do
661 elsif project_id_changed?
660 elsif project_id_changed?
662 if project.shared_versions.include?(fixed_version)
661 if project.shared_versions.include?(fixed_version)
663 versions << fixed_version
662 versions << fixed_version
664 end
663 end
665 else
664 else
666 versions << fixed_version
665 versions << fixed_version
667 end
666 end
668 end
667 end
669 @assignable_versions = versions.uniq.sort
668 @assignable_versions = versions.uniq.sort
670 end
669 end
671
670
672 # Returns true if this issue is blocked by another issue that is still open
671 # Returns true if this issue is blocked by another issue that is still open
673 def blocked?
672 def blocked?
674 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
673 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
675 end
674 end
676
675
677 # Returns an array of statuses that user is able to apply
676 # Returns an array of statuses that user is able to apply
678 def new_statuses_allowed_to(user=User.current, include_default=false)
677 def new_statuses_allowed_to(user=User.current, include_default=false)
679 if new_record? && @copied_from
678 if new_record? && @copied_from
680 [IssueStatus.default, @copied_from.status].compact.uniq.sort
679 [IssueStatus.default, @copied_from.status].compact.uniq.sort
681 else
680 else
682 initial_status = nil
681 initial_status = nil
683 if new_record?
682 if new_record?
684 initial_status = IssueStatus.default
683 initial_status = IssueStatus.default
685 elsif status_id_was
684 elsif status_id_was
686 initial_status = IssueStatus.find_by_id(status_id_was)
685 initial_status = IssueStatus.find_by_id(status_id_was)
687 end
686 end
688 initial_status ||= status
687 initial_status ||= status
689
688
690 statuses = initial_status.find_new_statuses_allowed_to(
689 statuses = initial_status.find_new_statuses_allowed_to(
691 user.admin ? Role.all : user.roles_for_project(project),
690 user.admin ? Role.all : user.roles_for_project(project),
692 tracker,
691 tracker,
693 author == user,
692 author == user,
694 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
693 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
695 )
694 )
696 statuses << initial_status unless statuses.empty?
695 statuses << initial_status unless statuses.empty?
697 statuses << IssueStatus.default if include_default
696 statuses << IssueStatus.default if include_default
698 statuses = statuses.compact.uniq.sort
697 statuses = statuses.compact.uniq.sort
699 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
698 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
700 end
699 end
701 end
700 end
702
701
703 def assigned_to_was
702 def assigned_to_was
704 if assigned_to_id_changed? && assigned_to_id_was.present?
703 if assigned_to_id_changed? && assigned_to_id_was.present?
705 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
704 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
706 end
705 end
707 end
706 end
708
707
709 # Returns the mail adresses of users that should be notified
708 # Returns the mail adresses of users that should be notified
710 def recipients
709 def recipients
711 notified = []
710 notified = []
712 # Author and assignee are always notified unless they have been
711 # Author and assignee are always notified unless they have been
713 # locked or don't want to be notified
712 # locked or don't want to be notified
714 notified << author if author
713 notified << author if author
715 if assigned_to
714 if assigned_to
716 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
715 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
717 end
716 end
718 if assigned_to_was
717 if assigned_to_was
719 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
718 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
720 end
719 end
721 notified = notified.select {|u| u.active? && u.notify_about?(self)}
720 notified = notified.select {|u| u.active? && u.notify_about?(self)}
722
721
723 notified += project.notified_users
722 notified += project.notified_users
724 notified.uniq!
723 notified.uniq!
725 # Remove users that can not view the issue
724 # Remove users that can not view the issue
726 notified.reject! {|user| !visible?(user)}
725 notified.reject! {|user| !visible?(user)}
727 notified.collect(&:mail)
726 notified.collect(&:mail)
728 end
727 end
729
728
730 # Returns the number of hours spent on this issue
729 # Returns the number of hours spent on this issue
731 def spent_hours
730 def spent_hours
732 @spent_hours ||= time_entries.sum(:hours) || 0
731 @spent_hours ||= time_entries.sum(:hours) || 0
733 end
732 end
734
733
735 # Returns the total number of hours spent on this issue and its descendants
734 # Returns the total number of hours spent on this issue and its descendants
736 #
735 #
737 # Example:
736 # Example:
738 # spent_hours => 0.0
737 # spent_hours => 0.0
739 # spent_hours => 50.2
738 # spent_hours => 50.2
740 def total_spent_hours
739 def total_spent_hours
741 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
740 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
742 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
741 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
743 end
742 end
744
743
745 def relations
744 def relations
746 @relations ||= (relations_from + relations_to).sort
745 @relations ||= (relations_from + relations_to).sort
747 end
746 end
748
747
749 # Preloads relations for a collection of issues
748 # Preloads relations for a collection of issues
750 def self.load_relations(issues)
749 def self.load_relations(issues)
751 if issues.any?
750 if issues.any?
752 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
751 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
753 issues.each do |issue|
752 issues.each do |issue|
754 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
753 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
755 end
754 end
756 end
755 end
757 end
756 end
758
757
759 # Preloads visible spent time for a collection of issues
758 # Preloads visible spent time for a collection of issues
760 def self.load_visible_spent_hours(issues, user=User.current)
759 def self.load_visible_spent_hours(issues, user=User.current)
761 if issues.any?
760 if issues.any?
762 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
761 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
763 issues.each do |issue|
762 issues.each do |issue|
764 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
763 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
765 end
764 end
766 end
765 end
767 end
766 end
768
767
769 # Finds an issue relation given its id.
768 # Finds an issue relation given its id.
770 def find_relation(relation_id)
769 def find_relation(relation_id)
771 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
770 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
772 end
771 end
773
772
774 def all_dependent_issues(except=[])
773 def all_dependent_issues(except=[])
775 except << self
774 except << self
776 dependencies = []
775 dependencies = []
777 relations_from.each do |relation|
776 relations_from.each do |relation|
778 if relation.issue_to && !except.include?(relation.issue_to)
777 if relation.issue_to && !except.include?(relation.issue_to)
779 dependencies << relation.issue_to
778 dependencies << relation.issue_to
780 dependencies += relation.issue_to.all_dependent_issues(except)
779 dependencies += relation.issue_to.all_dependent_issues(except)
781 end
780 end
782 end
781 end
783 dependencies
782 dependencies
784 end
783 end
785
784
786 # Returns an array of issues that duplicate this one
785 # Returns an array of issues that duplicate this one
787 def duplicates
786 def duplicates
788 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
787 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
789 end
788 end
790
789
791 # Returns the due date or the target due date if any
790 # Returns the due date or the target due date if any
792 # Used on gantt chart
791 # Used on gantt chart
793 def due_before
792 def due_before
794 due_date || (fixed_version ? fixed_version.effective_date : nil)
793 due_date || (fixed_version ? fixed_version.effective_date : nil)
795 end
794 end
796
795
797 # Returns the time scheduled for this issue.
796 # Returns the time scheduled for this issue.
798 #
797 #
799 # Example:
798 # Example:
800 # Start Date: 2/26/09, End Date: 3/04/09
799 # Start Date: 2/26/09, End Date: 3/04/09
801 # duration => 6
800 # duration => 6
802 def duration
801 def duration
803 (start_date && due_date) ? due_date - start_date : 0
802 (start_date && due_date) ? due_date - start_date : 0
804 end
803 end
805
804
806 def soonest_start
805 def soonest_start
807 @soonest_start ||= (
806 @soonest_start ||= (
808 relations_to.collect{|relation| relation.successor_soonest_start} +
807 relations_to.collect{|relation| relation.successor_soonest_start} +
809 ancestors.collect(&:soonest_start)
808 ancestors.collect(&:soonest_start)
810 ).compact.max
809 ).compact.max
811 end
810 end
812
811
813 def reschedule_after(date)
812 def reschedule_after(date)
814 return if date.nil?
813 return if date.nil?
815 if leaf?
814 if leaf?
816 if start_date.nil? || start_date < date
815 if start_date.nil? || start_date < date
817 self.start_date, self.due_date = date, date + duration
816 self.start_date, self.due_date = date, date + duration
818 begin
817 begin
819 save
818 save
820 rescue ActiveRecord::StaleObjectError
819 rescue ActiveRecord::StaleObjectError
821 reload
820 reload
822 self.start_date, self.due_date = date, date + duration
821 self.start_date, self.due_date = date, date + duration
823 save
822 save
824 end
823 end
825 end
824 end
826 else
825 else
827 leaves.each do |leaf|
826 leaves.each do |leaf|
828 leaf.reschedule_after(date)
827 leaf.reschedule_after(date)
829 end
828 end
830 end
829 end
831 end
830 end
832
831
833 def <=>(issue)
832 def <=>(issue)
834 if issue.nil?
833 if issue.nil?
835 -1
834 -1
836 elsif root_id != issue.root_id
835 elsif root_id != issue.root_id
837 (root_id || 0) <=> (issue.root_id || 0)
836 (root_id || 0) <=> (issue.root_id || 0)
838 else
837 else
839 (lft || 0) <=> (issue.lft || 0)
838 (lft || 0) <=> (issue.lft || 0)
840 end
839 end
841 end
840 end
842
841
843 def to_s
842 def to_s
844 "#{tracker} ##{id}: #{subject}"
843 "#{tracker} ##{id}: #{subject}"
845 end
844 end
846
845
847 # Returns a string of css classes that apply to the issue
846 # Returns a string of css classes that apply to the issue
848 def css_classes
847 def css_classes
849 s = "issue status-#{status_id} priority-#{priority_id}"
848 s = "issue status-#{status_id} priority-#{priority_id}"
850 s << ' closed' if closed?
849 s << ' closed' if closed?
851 s << ' overdue' if overdue?
850 s << ' overdue' if overdue?
852 s << ' child' if child?
851 s << ' child' if child?
853 s << ' parent' unless leaf?
852 s << ' parent' unless leaf?
854 s << ' private' if is_private?
853 s << ' private' if is_private?
855 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
854 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
856 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
855 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
857 s
856 s
858 end
857 end
859
858
860 # Saves an issue and a time_entry from the parameters
859 # Saves an issue and a time_entry from the parameters
861 def save_issue_with_child_records(params, existing_time_entry=nil)
860 def save_issue_with_child_records(params, existing_time_entry=nil)
862 Issue.transaction do
861 Issue.transaction do
863 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
862 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
864 @time_entry = existing_time_entry || TimeEntry.new
863 @time_entry = existing_time_entry || TimeEntry.new
865 @time_entry.project = project
864 @time_entry.project = project
866 @time_entry.issue = self
865 @time_entry.issue = self
867 @time_entry.user = User.current
866 @time_entry.user = User.current
868 @time_entry.spent_on = User.current.today
867 @time_entry.spent_on = User.current.today
869 @time_entry.attributes = params[:time_entry]
868 @time_entry.attributes = params[:time_entry]
870 self.time_entries << @time_entry
869 self.time_entries << @time_entry
871 end
870 end
872
871
873 # TODO: Rename hook
872 # TODO: Rename hook
874 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
873 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
875 if save
874 if save
876 # TODO: Rename hook
875 # TODO: Rename hook
877 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
876 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
878 else
877 else
879 raise ActiveRecord::Rollback
878 raise ActiveRecord::Rollback
880 end
879 end
881 end
880 end
882 end
881 end
883
882
884 # Unassigns issues from +version+ if it's no longer shared with issue's project
883 # Unassigns issues from +version+ if it's no longer shared with issue's project
885 def self.update_versions_from_sharing_change(version)
884 def self.update_versions_from_sharing_change(version)
886 # Update issues assigned to the version
885 # Update issues assigned to the version
887 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
886 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
888 end
887 end
889
888
890 # Unassigns issues from versions that are no longer shared
889 # Unassigns issues from versions that are no longer shared
891 # after +project+ was moved
890 # after +project+ was moved
892 def self.update_versions_from_hierarchy_change(project)
891 def self.update_versions_from_hierarchy_change(project)
893 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
892 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
894 # Update issues of the moved projects and issues assigned to a version of a moved project
893 # Update issues of the moved projects and issues assigned to a version of a moved project
895 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
894 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
896 end
895 end
897
896
898 def parent_issue_id=(arg)
897 def parent_issue_id=(arg)
899 parent_issue_id = arg.blank? ? nil : arg.to_i
898 parent_issue_id = arg.blank? ? nil : arg.to_i
900 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
899 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
901 @parent_issue.id
900 @parent_issue.id
902 else
901 else
903 @parent_issue = nil
902 @parent_issue = nil
904 nil
903 nil
905 end
904 end
906 end
905 end
907
906
908 def parent_issue_id
907 def parent_issue_id
909 if instance_variable_defined? :@parent_issue
908 if instance_variable_defined? :@parent_issue
910 @parent_issue.nil? ? nil : @parent_issue.id
909 @parent_issue.nil? ? nil : @parent_issue.id
911 else
910 else
912 parent_id
911 parent_id
913 end
912 end
914 end
913 end
915
914
916 # Extracted from the ReportsController.
915 # Extracted from the ReportsController.
917 def self.by_tracker(project)
916 def self.by_tracker(project)
918 count_and_group_by(:project => project,
917 count_and_group_by(:project => project,
919 :field => 'tracker_id',
918 :field => 'tracker_id',
920 :joins => Tracker.table_name)
919 :joins => Tracker.table_name)
921 end
920 end
922
921
923 def self.by_version(project)
922 def self.by_version(project)
924 count_and_group_by(:project => project,
923 count_and_group_by(:project => project,
925 :field => 'fixed_version_id',
924 :field => 'fixed_version_id',
926 :joins => Version.table_name)
925 :joins => Version.table_name)
927 end
926 end
928
927
929 def self.by_priority(project)
928 def self.by_priority(project)
930 count_and_group_by(:project => project,
929 count_and_group_by(:project => project,
931 :field => 'priority_id',
930 :field => 'priority_id',
932 :joins => IssuePriority.table_name)
931 :joins => IssuePriority.table_name)
933 end
932 end
934
933
935 def self.by_category(project)
934 def self.by_category(project)
936 count_and_group_by(:project => project,
935 count_and_group_by(:project => project,
937 :field => 'category_id',
936 :field => 'category_id',
938 :joins => IssueCategory.table_name)
937 :joins => IssueCategory.table_name)
939 end
938 end
940
939
941 def self.by_assigned_to(project)
940 def self.by_assigned_to(project)
942 count_and_group_by(:project => project,
941 count_and_group_by(:project => project,
943 :field => 'assigned_to_id',
942 :field => 'assigned_to_id',
944 :joins => User.table_name)
943 :joins => User.table_name)
945 end
944 end
946
945
947 def self.by_author(project)
946 def self.by_author(project)
948 count_and_group_by(:project => project,
947 count_and_group_by(:project => project,
949 :field => 'author_id',
948 :field => 'author_id',
950 :joins => User.table_name)
949 :joins => User.table_name)
951 end
950 end
952
951
953 def self.by_subproject(project)
952 def self.by_subproject(project)
954 ActiveRecord::Base.connection.select_all("select s.id as status_id,
953 ActiveRecord::Base.connection.select_all("select s.id as status_id,
955 s.is_closed as closed,
954 s.is_closed as closed,
956 #{Issue.table_name}.project_id as project_id,
955 #{Issue.table_name}.project_id as project_id,
957 count(#{Issue.table_name}.id) as total
956 count(#{Issue.table_name}.id) as total
958 from
957 from
959 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
958 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
960 where
959 where
961 #{Issue.table_name}.status_id=s.id
960 #{Issue.table_name}.status_id=s.id
962 and #{Issue.table_name}.project_id = #{Project.table_name}.id
961 and #{Issue.table_name}.project_id = #{Project.table_name}.id
963 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
962 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
964 and #{Issue.table_name}.project_id <> #{project.id}
963 and #{Issue.table_name}.project_id <> #{project.id}
965 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
964 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
966 end
965 end
967 # End ReportsController extraction
966 # End ReportsController extraction
968
967
969 # Returns an array of projects that user can assign the issue to
968 # Returns an array of projects that user can assign the issue to
970 def allowed_target_projects(user=User.current)
969 def allowed_target_projects(user=User.current)
971 if new_record?
970 if new_record?
972 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
971 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
973 else
972 else
974 self.class.allowed_target_projects_on_move(user)
973 self.class.allowed_target_projects_on_move(user)
975 end
974 end
976 end
975 end
977
976
978 # Returns an array of projects that user can move issues to
977 # Returns an array of projects that user can move issues to
979 def self.allowed_target_projects_on_move(user=User.current)
978 def self.allowed_target_projects_on_move(user=User.current)
980 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
979 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
981 end
980 end
982
981
983 private
982 private
984
983
985 def after_project_change
984 def after_project_change
986 # Update project_id on related time entries
985 # Update project_id on related time entries
987 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
986 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
988
987
989 # Delete issue relations
988 # Delete issue relations
990 unless Setting.cross_project_issue_relations?
989 unless Setting.cross_project_issue_relations?
991 relations_from.clear
990 relations_from.clear
992 relations_to.clear
991 relations_to.clear
993 end
992 end
994
993
995 # Move subtasks
994 # Move subtasks
996 children.each do |child|
995 children.each do |child|
997 # Change project and keep project
996 # Change project and keep project
998 child.send :project=, project, true
997 child.send :project=, project, true
999 unless child.save
998 unless child.save
1000 raise ActiveRecord::Rollback
999 raise ActiveRecord::Rollback
1001 end
1000 end
1002 end
1001 end
1003 end
1002 end
1004
1003
1005 def update_nested_set_attributes
1004 def update_nested_set_attributes
1006 if root_id.nil?
1005 if root_id.nil?
1007 # issue was just created
1006 # issue was just created
1008 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1007 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1009 set_default_left_and_right
1008 set_default_left_and_right
1010 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1009 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1011 if @parent_issue
1010 if @parent_issue
1012 move_to_child_of(@parent_issue)
1011 move_to_child_of(@parent_issue)
1013 end
1012 end
1014 reload
1013 reload
1015 elsif parent_issue_id != parent_id
1014 elsif parent_issue_id != parent_id
1016 former_parent_id = parent_id
1015 former_parent_id = parent_id
1017 # moving an existing issue
1016 # moving an existing issue
1018 if @parent_issue && @parent_issue.root_id == root_id
1017 if @parent_issue && @parent_issue.root_id == root_id
1019 # inside the same tree
1018 # inside the same tree
1020 move_to_child_of(@parent_issue)
1019 move_to_child_of(@parent_issue)
1021 else
1020 else
1022 # to another tree
1021 # to another tree
1023 unless root?
1022 unless root?
1024 move_to_right_of(root)
1023 move_to_right_of(root)
1025 reload
1024 reload
1026 end
1025 end
1027 old_root_id = root_id
1026 old_root_id = root_id
1028 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1027 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1029 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1028 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1030 offset = target_maxright + 1 - lft
1029 offset = target_maxright + 1 - lft
1031 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1030 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1032 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1031 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1033 self[left_column_name] = lft + offset
1032 self[left_column_name] = lft + offset
1034 self[right_column_name] = rgt + offset
1033 self[right_column_name] = rgt + offset
1035 if @parent_issue
1034 if @parent_issue
1036 move_to_child_of(@parent_issue)
1035 move_to_child_of(@parent_issue)
1037 end
1036 end
1038 end
1037 end
1039 reload
1038 reload
1040 # delete invalid relations of all descendants
1039 # delete invalid relations of all descendants
1041 self_and_descendants.each do |issue|
1040 self_and_descendants.each do |issue|
1042 issue.relations.each do |relation|
1041 issue.relations.each do |relation|
1043 relation.destroy unless relation.valid?
1042 relation.destroy unless relation.valid?
1044 end
1043 end
1045 end
1044 end
1046 # update former parent
1045 # update former parent
1047 recalculate_attributes_for(former_parent_id) if former_parent_id
1046 recalculate_attributes_for(former_parent_id) if former_parent_id
1048 end
1047 end
1049 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1048 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1050 end
1049 end
1051
1050
1052 def update_parent_attributes
1051 def update_parent_attributes
1053 recalculate_attributes_for(parent_id) if parent_id
1052 recalculate_attributes_for(parent_id) if parent_id
1054 end
1053 end
1055
1054
1056 def recalculate_attributes_for(issue_id)
1055 def recalculate_attributes_for(issue_id)
1057 if issue_id && p = Issue.find_by_id(issue_id)
1056 if issue_id && p = Issue.find_by_id(issue_id)
1058 # priority = highest priority of children
1057 # priority = highest priority of children
1059 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1058 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1060 p.priority = IssuePriority.find_by_position(priority_position)
1059 p.priority = IssuePriority.find_by_position(priority_position)
1061 end
1060 end
1062
1061
1063 # start/due dates = lowest/highest dates of children
1062 # start/due dates = lowest/highest dates of children
1064 p.start_date = p.children.minimum(:start_date)
1063 p.start_date = p.children.minimum(:start_date)
1065 p.due_date = p.children.maximum(:due_date)
1064 p.due_date = p.children.maximum(:due_date)
1066 if p.start_date && p.due_date && p.due_date < p.start_date
1065 if p.start_date && p.due_date && p.due_date < p.start_date
1067 p.start_date, p.due_date = p.due_date, p.start_date
1066 p.start_date, p.due_date = p.due_date, p.start_date
1068 end
1067 end
1069
1068
1070 # done ratio = weighted average ratio of leaves
1069 # done ratio = weighted average ratio of leaves
1071 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1070 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1072 leaves_count = p.leaves.count
1071 leaves_count = p.leaves.count
1073 if leaves_count > 0
1072 if leaves_count > 0
1074 average = p.leaves.average(:estimated_hours).to_f
1073 average = p.leaves.average(:estimated_hours).to_f
1075 if average == 0
1074 if average == 0
1076 average = 1
1075 average = 1
1077 end
1076 end
1078 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1077 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1079 progress = done / (average * leaves_count)
1078 progress = done / (average * leaves_count)
1080 p.done_ratio = progress.round
1079 p.done_ratio = progress.round
1081 end
1080 end
1082 end
1081 end
1083
1082
1084 # estimate = sum of leaves estimates
1083 # estimate = sum of leaves estimates
1085 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1084 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1086 p.estimated_hours = nil if p.estimated_hours == 0.0
1085 p.estimated_hours = nil if p.estimated_hours == 0.0
1087
1086
1088 # ancestors will be recursively updated
1087 # ancestors will be recursively updated
1089 p.save(:validate => false)
1088 p.save(:validate => false)
1090 end
1089 end
1091 end
1090 end
1092
1091
1093 # Update issues so their versions are not pointing to a
1092 # Update issues so their versions are not pointing to a
1094 # fixed_version that is not shared with the issue's project
1093 # fixed_version that is not shared with the issue's project
1095 def self.update_versions(conditions=nil)
1094 def self.update_versions(conditions=nil)
1096 # Only need to update issues with a fixed_version from
1095 # Only need to update issues with a fixed_version from
1097 # a different project and that is not systemwide shared
1096 # a different project and that is not systemwide shared
1098 Issue.scoped(:conditions => conditions).all(
1097 Issue.scoped(:conditions => conditions).all(
1099 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1098 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1100 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1099 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1101 " AND #{Version.table_name}.sharing <> 'system'",
1100 " AND #{Version.table_name}.sharing <> 'system'",
1102 :include => [:project, :fixed_version]
1101 :include => [:project, :fixed_version]
1103 ).each do |issue|
1102 ).each do |issue|
1104 next if issue.project.nil? || issue.fixed_version.nil?
1103 next if issue.project.nil? || issue.fixed_version.nil?
1105 unless issue.project.shared_versions.include?(issue.fixed_version)
1104 unless issue.project.shared_versions.include?(issue.fixed_version)
1106 issue.init_journal(User.current)
1105 issue.init_journal(User.current)
1107 issue.fixed_version = nil
1106 issue.fixed_version = nil
1108 issue.save
1107 issue.save
1109 end
1108 end
1110 end
1109 end
1111 end
1110 end
1112
1111
1113 # Callback on attachment deletion
1112 # Callback on attachment deletion
1114 def attachment_added(obj)
1113 def attachment_added(obj)
1115 if @current_journal && !obj.new_record?
1114 if @current_journal && !obj.new_record?
1116 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1115 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1117 end
1116 end
1118 end
1117 end
1119
1118
1120 # Callback on attachment deletion
1119 # Callback on attachment deletion
1121 def attachment_removed(obj)
1120 def attachment_removed(obj)
1122 if @current_journal && !obj.new_record?
1121 if @current_journal && !obj.new_record?
1123 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1122 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1124 @current_journal.save
1123 @current_journal.save
1125 end
1124 end
1126 end
1125 end
1127
1126
1128 # Default assignment based on category
1127 # Default assignment based on category
1129 def default_assign
1128 def default_assign
1130 if assigned_to.nil? && category && category.assigned_to
1129 if assigned_to.nil? && category && category.assigned_to
1131 self.assigned_to = category.assigned_to
1130 self.assigned_to = category.assigned_to
1132 end
1131 end
1133 end
1132 end
1134
1133
1135 # Updates start/due dates of following issues
1134 # Updates start/due dates of following issues
1136 def reschedule_following_issues
1135 def reschedule_following_issues
1137 if start_date_changed? || due_date_changed?
1136 if start_date_changed? || due_date_changed?
1138 relations_from.each do |relation|
1137 relations_from.each do |relation|
1139 relation.set_issue_to_dates
1138 relation.set_issue_to_dates
1140 end
1139 end
1141 end
1140 end
1142 end
1141 end
1143
1142
1144 # Closes duplicates if the issue is being closed
1143 # Closes duplicates if the issue is being closed
1145 def close_duplicates
1144 def close_duplicates
1146 if closing?
1145 if closing?
1147 duplicates.each do |duplicate|
1146 duplicates.each do |duplicate|
1148 # Reload is need in case the duplicate was updated by a previous duplicate
1147 # Reload is need in case the duplicate was updated by a previous duplicate
1149 duplicate.reload
1148 duplicate.reload
1150 # Don't re-close it if it's already closed
1149 # Don't re-close it if it's already closed
1151 next if duplicate.closed?
1150 next if duplicate.closed?
1152 # Same user and notes
1151 # Same user and notes
1153 if @current_journal
1152 if @current_journal
1154 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1153 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1155 end
1154 end
1156 duplicate.update_attribute :status, self.status
1155 duplicate.update_attribute :status, self.status
1157 end
1156 end
1158 end
1157 end
1159 end
1158 end
1160
1159
1161 # Make sure updated_on is updated when adding a note
1160 # Make sure updated_on is updated when adding a note
1162 def force_updated_on_change
1161 def force_updated_on_change
1163 if @current_journal
1162 if @current_journal
1164 self.updated_on = current_time_from_proper_timezone
1163 self.updated_on = current_time_from_proper_timezone
1165 end
1164 end
1166 end
1165 end
1167
1166
1168 # Saves the changes in a Journal
1167 # Saves the changes in a Journal
1169 # Called after_save
1168 # Called after_save
1170 def create_journal
1169 def create_journal
1171 if @current_journal
1170 if @current_journal
1172 # attributes changes
1171 # attributes changes
1173 if @attributes_before_change
1172 if @attributes_before_change
1174 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1173 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1175 before = @attributes_before_change[c]
1174 before = @attributes_before_change[c]
1176 after = send(c)
1175 after = send(c)
1177 next if before == after || (before.blank? && after.blank?)
1176 next if before == after || (before.blank? && after.blank?)
1178 @current_journal.details << JournalDetail.new(:property => 'attr',
1177 @current_journal.details << JournalDetail.new(:property => 'attr',
1179 :prop_key => c,
1178 :prop_key => c,
1180 :old_value => before,
1179 :old_value => before,
1181 :value => after)
1180 :value => after)
1182 }
1181 }
1183 end
1182 end
1184 if @custom_values_before_change
1183 if @custom_values_before_change
1185 # custom fields changes
1184 # custom fields changes
1186 custom_field_values.each {|c|
1185 custom_field_values.each {|c|
1187 before = @custom_values_before_change[c.custom_field_id]
1186 before = @custom_values_before_change[c.custom_field_id]
1188 after = c.value
1187 after = c.value
1189 next if before == after || (before.blank? && after.blank?)
1188 next if before == after || (before.blank? && after.blank?)
1190
1189
1191 if before.is_a?(Array) || after.is_a?(Array)
1190 if before.is_a?(Array) || after.is_a?(Array)
1192 before = [before] unless before.is_a?(Array)
1191 before = [before] unless before.is_a?(Array)
1193 after = [after] unless after.is_a?(Array)
1192 after = [after] unless after.is_a?(Array)
1194
1193
1195 # values removed
1194 # values removed
1196 (before - after).reject(&:blank?).each do |value|
1195 (before - after).reject(&:blank?).each do |value|
1197 @current_journal.details << JournalDetail.new(:property => 'cf',
1196 @current_journal.details << JournalDetail.new(:property => 'cf',
1198 :prop_key => c.custom_field_id,
1197 :prop_key => c.custom_field_id,
1199 :old_value => value,
1198 :old_value => value,
1200 :value => nil)
1199 :value => nil)
1201 end
1200 end
1202 # values added
1201 # values added
1203 (after - before).reject(&:blank?).each do |value|
1202 (after - before).reject(&:blank?).each do |value|
1204 @current_journal.details << JournalDetail.new(:property => 'cf',
1203 @current_journal.details << JournalDetail.new(:property => 'cf',
1205 :prop_key => c.custom_field_id,
1204 :prop_key => c.custom_field_id,
1206 :old_value => nil,
1205 :old_value => nil,
1207 :value => value)
1206 :value => value)
1208 end
1207 end
1209 else
1208 else
1210 @current_journal.details << JournalDetail.new(:property => 'cf',
1209 @current_journal.details << JournalDetail.new(:property => 'cf',
1211 :prop_key => c.custom_field_id,
1210 :prop_key => c.custom_field_id,
1212 :old_value => before,
1211 :old_value => before,
1213 :value => after)
1212 :value => after)
1214 end
1213 end
1215 }
1214 }
1216 end
1215 end
1217 @current_journal.save
1216 @current_journal.save
1218 # reset current journal
1217 # reset current journal
1219 init_journal @current_journal.user, @current_journal.notes
1218 init_journal @current_journal.user, @current_journal.notes
1220 end
1219 end
1221 end
1220 end
1222
1221
1223 # Query generator for selecting groups of issue counts for a project
1222 # Query generator for selecting groups of issue counts for a project
1224 # based on specific criteria
1223 # based on specific criteria
1225 #
1224 #
1226 # Options
1225 # Options
1227 # * project - Project to search in.
1226 # * project - Project to search in.
1228 # * field - String. Issue field to key off of in the grouping.
1227 # * field - String. Issue field to key off of in the grouping.
1229 # * joins - String. The table name to join against.
1228 # * joins - String. The table name to join against.
1230 def self.count_and_group_by(options)
1229 def self.count_and_group_by(options)
1231 project = options.delete(:project)
1230 project = options.delete(:project)
1232 select_field = options.delete(:field)
1231 select_field = options.delete(:field)
1233 joins = options.delete(:joins)
1232 joins = options.delete(:joins)
1234
1233
1235 where = "#{Issue.table_name}.#{select_field}=j.id"
1234 where = "#{Issue.table_name}.#{select_field}=j.id"
1236
1235
1237 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1236 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1238 s.is_closed as closed,
1237 s.is_closed as closed,
1239 j.id as #{select_field},
1238 j.id as #{select_field},
1240 count(#{Issue.table_name}.id) as total
1239 count(#{Issue.table_name}.id) as total
1241 from
1240 from
1242 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1241 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1243 where
1242 where
1244 #{Issue.table_name}.status_id=s.id
1243 #{Issue.table_name}.status_id=s.id
1245 and #{where}
1244 and #{where}
1246 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1245 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1247 and #{visible_condition(User.current, :project => project)}
1246 and #{visible_condition(User.current, :project => project)}
1248 group by s.id, s.is_closed, j.id")
1247 group by s.id, s.is_closed, j.id")
1249 end
1248 end
1250 end
1249 end
@@ -1,279 +1,278
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 :delete_permission => :manage_files
25 :delete_permission => :manage_files
26
26
27 VERSION_STATUSES = %w(open locked closed)
27 VERSION_STATUSES = %w(open locked closed)
28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
28 VERSION_SHARINGS = %w(none descendants hierarchy tree system)
29
29
30 validates_presence_of :name
30 validates_presence_of :name
31 validates_uniqueness_of :name, :scope => [:project_id]
31 validates_uniqueness_of :name, :scope => [:project_id]
32 validates_length_of :name, :maximum => 60
32 validates_length_of :name, :maximum => 60
33 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
33 validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => :not_a_date, :allow_nil => true
34 validates_inclusion_of :status, :in => VERSION_STATUSES
34 validates_inclusion_of :status, :in => VERSION_STATUSES
35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
35 validates_inclusion_of :sharing, :in => VERSION_SHARINGS
36
36
37 scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
37 scope :named, lambda {|arg| { :conditions => ["LOWER(#{table_name}.name) = LOWER(?)", arg.to_s.strip]}}
38 class << self; undef :open; end
39 scope :open, :conditions => {:status => 'open'}
38 scope :open, :conditions => {:status => 'open'}
40 scope :visible, lambda {|*args| { :include => :project,
39 scope :visible, lambda {|*args| { :include => :project,
41 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
40 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
42
41
43 safe_attributes 'name',
42 safe_attributes 'name',
44 'description',
43 'description',
45 'effective_date',
44 'effective_date',
46 'due_date',
45 'due_date',
47 'wiki_page_title',
46 'wiki_page_title',
48 'status',
47 'status',
49 'sharing',
48 'sharing',
50 'custom_field_values'
49 'custom_field_values'
51
50
52 # Returns true if +user+ or current user is allowed to view the version
51 # Returns true if +user+ or current user is allowed to view the version
53 def visible?(user=User.current)
52 def visible?(user=User.current)
54 user.allowed_to?(:view_issues, self.project)
53 user.allowed_to?(:view_issues, self.project)
55 end
54 end
56
55
57 # Version files have same visibility as project files
56 # Version files have same visibility as project files
58 def attachments_visible?(*args)
57 def attachments_visible?(*args)
59 project.present? && project.attachments_visible?(*args)
58 project.present? && project.attachments_visible?(*args)
60 end
59 end
61
60
62 def start_date
61 def start_date
63 @start_date ||= fixed_issues.minimum('start_date')
62 @start_date ||= fixed_issues.minimum('start_date')
64 end
63 end
65
64
66 def due_date
65 def due_date
67 effective_date
66 effective_date
68 end
67 end
69
68
70 def due_date=(arg)
69 def due_date=(arg)
71 self.effective_date=(arg)
70 self.effective_date=(arg)
72 end
71 end
73
72
74 # Returns the total estimated time for this version
73 # Returns the total estimated time for this version
75 # (sum of leaves estimated_hours)
74 # (sum of leaves estimated_hours)
76 def estimated_hours
75 def estimated_hours
77 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
76 @estimated_hours ||= fixed_issues.leaves.sum(:estimated_hours).to_f
78 end
77 end
79
78
80 # Returns the total reported time for this version
79 # Returns the total reported time for this version
81 def spent_hours
80 def spent_hours
82 @spent_hours ||= TimeEntry.sum(:hours, :joins => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
81 @spent_hours ||= TimeEntry.sum(:hours, :joins => :issue, :conditions => ["#{Issue.table_name}.fixed_version_id = ?", id]).to_f
83 end
82 end
84
83
85 def closed?
84 def closed?
86 status == 'closed'
85 status == 'closed'
87 end
86 end
88
87
89 def open?
88 def open?
90 status == 'open'
89 status == 'open'
91 end
90 end
92
91
93 # Returns true if the version is completed: due date reached and no open issues
92 # Returns true if the version is completed: due date reached and no open issues
94 def completed?
93 def completed?
95 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
94 effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
96 end
95 end
97
96
98 def behind_schedule?
97 def behind_schedule?
99 if completed_pourcent == 100
98 if completed_pourcent == 100
100 return false
99 return false
101 elsif due_date && start_date
100 elsif due_date && start_date
102 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
101 done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
103 return done_date <= Date.today
102 return done_date <= Date.today
104 else
103 else
105 false # No issues so it's not late
104 false # No issues so it's not late
106 end
105 end
107 end
106 end
108
107
109 # Returns the completion percentage of this version based on the amount of open/closed issues
108 # Returns the completion percentage of this version based on the amount of open/closed issues
110 # and the time spent on the open issues.
109 # and the time spent on the open issues.
111 def completed_pourcent
110 def completed_pourcent
112 if issues_count == 0
111 if issues_count == 0
113 0
112 0
114 elsif open_issues_count == 0
113 elsif open_issues_count == 0
115 100
114 100
116 else
115 else
117 issues_progress(false) + issues_progress(true)
116 issues_progress(false) + issues_progress(true)
118 end
117 end
119 end
118 end
120
119
121 # Returns the percentage of issues that have been marked as 'closed'.
120 # Returns the percentage of issues that have been marked as 'closed'.
122 def closed_pourcent
121 def closed_pourcent
123 if issues_count == 0
122 if issues_count == 0
124 0
123 0
125 else
124 else
126 issues_progress(false)
125 issues_progress(false)
127 end
126 end
128 end
127 end
129
128
130 # Returns true if the version is overdue: due date reached and some open issues
129 # Returns true if the version is overdue: due date reached and some open issues
131 def overdue?
130 def overdue?
132 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
131 effective_date && (effective_date < Date.today) && (open_issues_count > 0)
133 end
132 end
134
133
135 # Returns assigned issues count
134 # Returns assigned issues count
136 def issues_count
135 def issues_count
137 load_issue_counts
136 load_issue_counts
138 @issue_count
137 @issue_count
139 end
138 end
140
139
141 # Returns the total amount of open issues for this version.
140 # Returns the total amount of open issues for this version.
142 def open_issues_count
141 def open_issues_count
143 load_issue_counts
142 load_issue_counts
144 @open_issues_count
143 @open_issues_count
145 end
144 end
146
145
147 # Returns the total amount of closed issues for this version.
146 # Returns the total amount of closed issues for this version.
148 def closed_issues_count
147 def closed_issues_count
149 load_issue_counts
148 load_issue_counts
150 @closed_issues_count
149 @closed_issues_count
151 end
150 end
152
151
153 def wiki_page
152 def wiki_page
154 if project.wiki && !wiki_page_title.blank?
153 if project.wiki && !wiki_page_title.blank?
155 @wiki_page ||= project.wiki.find_page(wiki_page_title)
154 @wiki_page ||= project.wiki.find_page(wiki_page_title)
156 end
155 end
157 @wiki_page
156 @wiki_page
158 end
157 end
159
158
160 def to_s; name end
159 def to_s; name end
161
160
162 def to_s_with_project
161 def to_s_with_project
163 "#{project} - #{name}"
162 "#{project} - #{name}"
164 end
163 end
165
164
166 # Versions are sorted by effective_date and name
165 # Versions are sorted by effective_date and name
167 # Those with no effective_date are at the end, sorted by name
166 # Those with no effective_date are at the end, sorted by name
168 def <=>(version)
167 def <=>(version)
169 if self.effective_date
168 if self.effective_date
170 if version.effective_date
169 if version.effective_date
171 if self.effective_date == version.effective_date
170 if self.effective_date == version.effective_date
172 name == version.name ? id <=> version.id : name <=> version.name
171 name == version.name ? id <=> version.id : name <=> version.name
173 else
172 else
174 self.effective_date <=> version.effective_date
173 self.effective_date <=> version.effective_date
175 end
174 end
176 else
175 else
177 -1
176 -1
178 end
177 end
179 else
178 else
180 if version.effective_date
179 if version.effective_date
181 1
180 1
182 else
181 else
183 name == version.name ? id <=> version.id : name <=> version.name
182 name == version.name ? id <=> version.id : name <=> version.name
184 end
183 end
185 end
184 end
186 end
185 end
187
186
188 def self.fields_for_order_statement(table=nil)
187 def self.fields_for_order_statement(table=nil)
189 table ||= table_name
188 table ||= table_name
190 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
189 ["(CASE WHEN #{table}.effective_date IS NULL THEN 1 ELSE 0 END)", "#{table}.effective_date", "#{table}.name", "#{table}.id"]
191 end
190 end
192
191
193 scope :sorted, order(fields_for_order_statement)
192 scope :sorted, order(fields_for_order_statement)
194
193
195 # Returns the sharings that +user+ can set the version to
194 # Returns the sharings that +user+ can set the version to
196 def allowed_sharings(user = User.current)
195 def allowed_sharings(user = User.current)
197 VERSION_SHARINGS.select do |s|
196 VERSION_SHARINGS.select do |s|
198 if sharing == s
197 if sharing == s
199 true
198 true
200 else
199 else
201 case s
200 case s
202 when 'system'
201 when 'system'
203 # Only admin users can set a systemwide sharing
202 # Only admin users can set a systemwide sharing
204 user.admin?
203 user.admin?
205 when 'hierarchy', 'tree'
204 when 'hierarchy', 'tree'
206 # Only users allowed to manage versions of the root project can
205 # Only users allowed to manage versions of the root project can
207 # set sharing to hierarchy or tree
206 # set sharing to hierarchy or tree
208 project.nil? || user.allowed_to?(:manage_versions, project.root)
207 project.nil? || user.allowed_to?(:manage_versions, project.root)
209 else
208 else
210 true
209 true
211 end
210 end
212 end
211 end
213 end
212 end
214 end
213 end
215
214
216 private
215 private
217
216
218 def load_issue_counts
217 def load_issue_counts
219 unless @issue_count
218 unless @issue_count
220 @open_issues_count = 0
219 @open_issues_count = 0
221 @closed_issues_count = 0
220 @closed_issues_count = 0
222 fixed_issues.count(:all, :group => :status).each do |status, count|
221 fixed_issues.count(:all, :group => :status).each do |status, count|
223 if status.is_closed?
222 if status.is_closed?
224 @closed_issues_count += count
223 @closed_issues_count += count
225 else
224 else
226 @open_issues_count += count
225 @open_issues_count += count
227 end
226 end
228 end
227 end
229 @issue_count = @open_issues_count + @closed_issues_count
228 @issue_count = @open_issues_count + @closed_issues_count
230 end
229 end
231 end
230 end
232
231
233 # Update the issue's fixed versions. Used if a version's sharing changes.
232 # Update the issue's fixed versions. Used if a version's sharing changes.
234 def update_issues_from_sharing_change
233 def update_issues_from_sharing_change
235 if sharing_changed?
234 if sharing_changed?
236 if VERSION_SHARINGS.index(sharing_was).nil? ||
235 if VERSION_SHARINGS.index(sharing_was).nil? ||
237 VERSION_SHARINGS.index(sharing).nil? ||
236 VERSION_SHARINGS.index(sharing).nil? ||
238 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
237 VERSION_SHARINGS.index(sharing_was) > VERSION_SHARINGS.index(sharing)
239 Issue.update_versions_from_sharing_change self
238 Issue.update_versions_from_sharing_change self
240 end
239 end
241 end
240 end
242 end
241 end
243
242
244 # Returns the average estimated time of assigned issues
243 # Returns the average estimated time of assigned issues
245 # or 1 if no issue has an estimated time
244 # or 1 if no issue has an estimated time
246 # Used to weigth unestimated issues in progress calculation
245 # Used to weigth unestimated issues in progress calculation
247 def estimated_average
246 def estimated_average
248 if @estimated_average.nil?
247 if @estimated_average.nil?
249 average = fixed_issues.average(:estimated_hours).to_f
248 average = fixed_issues.average(:estimated_hours).to_f
250 if average == 0
249 if average == 0
251 average = 1
250 average = 1
252 end
251 end
253 @estimated_average = average
252 @estimated_average = average
254 end
253 end
255 @estimated_average
254 @estimated_average
256 end
255 end
257
256
258 # Returns the total progress of open or closed issues. The returned percentage takes into account
257 # Returns the total progress of open or closed issues. The returned percentage takes into account
259 # the amount of estimated time set for this version.
258 # the amount of estimated time set for this version.
260 #
259 #
261 # Examples:
260 # Examples:
262 # issues_progress(true) => returns the progress percentage for open issues.
261 # issues_progress(true) => returns the progress percentage for open issues.
263 # issues_progress(false) => returns the progress percentage for closed issues.
262 # issues_progress(false) => returns the progress percentage for closed issues.
264 def issues_progress(open)
263 def issues_progress(open)
265 @issues_progress ||= {}
264 @issues_progress ||= {}
266 @issues_progress[open] ||= begin
265 @issues_progress[open] ||= begin
267 progress = 0
266 progress = 0
268 if issues_count > 0
267 if issues_count > 0
269 ratio = open ? 'done_ratio' : 100
268 ratio = open ? 'done_ratio' : 100
270
269
271 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
270 done = fixed_issues.sum("COALESCE(estimated_hours, #{estimated_average}) * #{ratio}",
272 :joins => :status,
271 :joins => :status,
273 :conditions => ["#{IssueStatus.table_name}.is_closed = ?", !open]).to_f
272 :conditions => ["#{IssueStatus.table_name}.is_closed = ?", !open]).to_f
274 progress = done / (estimated_average * issues_count)
273 progress = done / (estimated_average * issues_count)
275 end
274 end
276 progress
275 progress
277 end
276 end
278 end
277 end
279 end
278 end
@@ -1,105 +1,110
1 require 'active_record'
1 require 'active_record'
2
2
3 module ActiveRecord
3 module ActiveRecord
4 class Base
4 class Base
5 include Redmine::I18n
5 include Redmine::I18n
6 # Translate attribute names for validation errors display
6 # Translate attribute names for validation errors display
7 def self.human_attribute_name(attr, *args)
7 def self.human_attribute_name(attr, *args)
8 attr = attr.to_s.sub(/_id$/, '')
8 attr = attr.to_s.sub(/_id$/, '')
9
9
10 l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr])
10 l("field_#{name.underscore.gsub('/', '_')}_#{attr}", :default => ["field_#{attr}".to_sym, attr])
11 end
11 end
12 end
12 end
13
14 # Undefines private Kernel#open method to allow using `open` scopes in models.
15 # See Defect #11545 (http://www.redmine.org/issues/11545) for details.
16 class Base ; undef open ; end
17 class Relation ; undef open ; end
13 end
18 end
14
19
15 module ActionView
20 module ActionView
16 module Helpers
21 module Helpers
17 module DateHelper
22 module DateHelper
18 # distance_of_time_in_words breaks when difference is greater than 30 years
23 # distance_of_time_in_words breaks when difference is greater than 30 years
19 def distance_of_date_in_words(from_date, to_date = 0, options = {})
24 def distance_of_date_in_words(from_date, to_date = 0, options = {})
20 from_date = from_date.to_date if from_date.respond_to?(:to_date)
25 from_date = from_date.to_date if from_date.respond_to?(:to_date)
21 to_date = to_date.to_date if to_date.respond_to?(:to_date)
26 to_date = to_date.to_date if to_date.respond_to?(:to_date)
22 distance_in_days = (to_date - from_date).abs
27 distance_in_days = (to_date - from_date).abs
23
28
24 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
29 I18n.with_options :locale => options[:locale], :scope => :'datetime.distance_in_words' do |locale|
25 case distance_in_days
30 case distance_in_days
26 when 0..60 then locale.t :x_days, :count => distance_in_days.round
31 when 0..60 then locale.t :x_days, :count => distance_in_days.round
27 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
32 when 61..720 then locale.t :about_x_months, :count => (distance_in_days / 30).round
28 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
33 else locale.t :over_x_years, :count => (distance_in_days / 365).floor
29 end
34 end
30 end
35 end
31 end
36 end
32 end
37 end
33 end
38 end
34
39
35 class Resolver
40 class Resolver
36 def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
41 def find_all(name, prefix=nil, partial=false, details={}, key=nil, locals=[])
37 cached(key, [name, prefix, partial], details, locals) do
42 cached(key, [name, prefix, partial], details, locals) do
38 if details[:formats] & [:xml, :json]
43 if details[:formats] & [:xml, :json]
39 details = details.dup
44 details = details.dup
40 details[:formats] = details[:formats].dup + [:api]
45 details[:formats] = details[:formats].dup + [:api]
41 end
46 end
42 find_templates(name, prefix, partial, details)
47 find_templates(name, prefix, partial, details)
43 end
48 end
44 end
49 end
45 end
50 end
46 end
51 end
47
52
48 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| html_tag || ''.html_safe }
53 ActionView::Base.field_error_proc = Proc.new{ |html_tag, instance| html_tag || ''.html_safe }
49
54
50 require 'mail'
55 require 'mail'
51
56
52 module DeliveryMethods
57 module DeliveryMethods
53 class AsyncSMTP < ::Mail::SMTP
58 class AsyncSMTP < ::Mail::SMTP
54 def deliver!(*args)
59 def deliver!(*args)
55 Thread.start do
60 Thread.start do
56 super *args
61 super *args
57 end
62 end
58 end
63 end
59 end
64 end
60
65
61 class AsyncSendmail < ::Mail::Sendmail
66 class AsyncSendmail < ::Mail::Sendmail
62 def deliver!(*args)
67 def deliver!(*args)
63 Thread.start do
68 Thread.start do
64 super *args
69 super *args
65 end
70 end
66 end
71 end
67 end
72 end
68
73
69 class TmpFile
74 class TmpFile
70 def initialize(*args); end
75 def initialize(*args); end
71
76
72 def deliver!(mail)
77 def deliver!(mail)
73 dest_dir = File.join(Rails.root, 'tmp', 'emails')
78 dest_dir = File.join(Rails.root, 'tmp', 'emails')
74 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
79 Dir.mkdir(dest_dir) unless File.directory?(dest_dir)
75 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
80 File.open(File.join(dest_dir, mail.message_id.gsub(/[<>]/, '') + '.eml'), 'wb') {|f| f.write(mail.encoded) }
76 end
81 end
77 end
82 end
78 end
83 end
79
84
80 ActionMailer::Base.add_delivery_method :async_smtp, DeliveryMethods::AsyncSMTP
85 ActionMailer::Base.add_delivery_method :async_smtp, DeliveryMethods::AsyncSMTP
81 ActionMailer::Base.add_delivery_method :async_sendmail, DeliveryMethods::AsyncSendmail
86 ActionMailer::Base.add_delivery_method :async_sendmail, DeliveryMethods::AsyncSendmail
82 ActionMailer::Base.add_delivery_method :tmp_file, DeliveryMethods::TmpFile
87 ActionMailer::Base.add_delivery_method :tmp_file, DeliveryMethods::TmpFile
83
88
84 module ActionController
89 module ActionController
85 module MimeResponds
90 module MimeResponds
86 class Collector
91 class Collector
87 def api(&block)
92 def api(&block)
88 any(:xml, :json, &block)
93 any(:xml, :json, &block)
89 end
94 end
90 end
95 end
91 end
96 end
92 end
97 end
93
98
94 module ActionController
99 module ActionController
95 class Base
100 class Base
96 # Displays an explicit message instead of a NoMethodError exception
101 # Displays an explicit message instead of a NoMethodError exception
97 # when trying to start Redmine with an old session_store.rb
102 # when trying to start Redmine with an old session_store.rb
98 # TODO: remove it in a later version
103 # TODO: remove it in a later version
99 def self.session=(*args)
104 def self.session=(*args)
100 $stderr.puts "Please remove config/initializers/session_store.rb and run `rake generate_secret_token`.\n" +
105 $stderr.puts "Please remove config/initializers/session_store.rb and run `rake generate_secret_token`.\n" +
101 "Setting the session secret with ActionController.session= is no longer supported in Rails 3."
106 "Setting the session secret with ActionController.session= is no longer supported in Rails 3."
102 exit 1
107 exit 1
103 end
108 end
104 end
109 end
105 end
110 end
@@ -1,1176 +1,1180
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :journals, :journal_details,
22 :journals, :journal_details,
23 :enumerations, :users, :issue_categories,
23 :enumerations, :users, :issue_categories,
24 :projects_trackers,
24 :projects_trackers,
25 :custom_fields,
25 :custom_fields,
26 :custom_fields_projects,
26 :custom_fields_projects,
27 :custom_fields_trackers,
27 :custom_fields_trackers,
28 :custom_values,
28 :custom_values,
29 :roles,
29 :roles,
30 :member_roles,
30 :member_roles,
31 :members,
31 :members,
32 :enabled_modules,
32 :enabled_modules,
33 :workflows,
33 :workflows,
34 :versions,
34 :versions,
35 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
35 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
36 :groups_users,
36 :groups_users,
37 :boards,
37 :boards,
38 :repositories
38 :repositories
39
39
40 def setup
40 def setup
41 @ecookbook = Project.find(1)
41 @ecookbook = Project.find(1)
42 @ecookbook_sub1 = Project.find(3)
42 @ecookbook_sub1 = Project.find(3)
43 set_tmp_attachments_directory
43 set_tmp_attachments_directory
44 User.current = nil
44 User.current = nil
45 end
45 end
46
46
47 def test_truth
47 def test_truth
48 assert_kind_of Project, @ecookbook
48 assert_kind_of Project, @ecookbook
49 assert_equal "eCookbook", @ecookbook.name
49 assert_equal "eCookbook", @ecookbook.name
50 end
50 end
51
51
52 def test_default_attributes
52 def test_default_attributes
53 with_settings :default_projects_public => '1' do
53 with_settings :default_projects_public => '1' do
54 assert_equal true, Project.new.is_public
54 assert_equal true, Project.new.is_public
55 assert_equal false, Project.new(:is_public => false).is_public
55 assert_equal false, Project.new(:is_public => false).is_public
56 end
56 end
57
57
58 with_settings :default_projects_public => '0' do
58 with_settings :default_projects_public => '0' do
59 assert_equal false, Project.new.is_public
59 assert_equal false, Project.new.is_public
60 assert_equal true, Project.new(:is_public => true).is_public
60 assert_equal true, Project.new(:is_public => true).is_public
61 end
61 end
62
62
63 with_settings :sequential_project_identifiers => '1' do
63 with_settings :sequential_project_identifiers => '1' do
64 assert !Project.new.identifier.blank?
64 assert !Project.new.identifier.blank?
65 assert Project.new(:identifier => '').identifier.blank?
65 assert Project.new(:identifier => '').identifier.blank?
66 end
66 end
67
67
68 with_settings :sequential_project_identifiers => '0' do
68 with_settings :sequential_project_identifiers => '0' do
69 assert Project.new.identifier.blank?
69 assert Project.new.identifier.blank?
70 assert !Project.new(:identifier => 'test').blank?
70 assert !Project.new(:identifier => 'test').blank?
71 end
71 end
72
72
73 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
73 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
74 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
74 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
75 end
75 end
76
76
77 assert_equal Tracker.all.sort, Project.new.trackers.sort
77 assert_equal Tracker.all.sort, Project.new.trackers.sort
78 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
78 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
79 end
79 end
80
80
81 def test_update
81 def test_update
82 assert_equal "eCookbook", @ecookbook.name
82 assert_equal "eCookbook", @ecookbook.name
83 @ecookbook.name = "eCook"
83 @ecookbook.name = "eCook"
84 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
84 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
85 @ecookbook.reload
85 @ecookbook.reload
86 assert_equal "eCook", @ecookbook.name
86 assert_equal "eCook", @ecookbook.name
87 end
87 end
88
88
89 def test_validate_identifier
89 def test_validate_identifier
90 to_test = {"abc" => true,
90 to_test = {"abc" => true,
91 "ab12" => true,
91 "ab12" => true,
92 "ab-12" => true,
92 "ab-12" => true,
93 "ab_12" => true,
93 "ab_12" => true,
94 "12" => false,
94 "12" => false,
95 "new" => false}
95 "new" => false}
96
96
97 to_test.each do |identifier, valid|
97 to_test.each do |identifier, valid|
98 p = Project.new
98 p = Project.new
99 p.identifier = identifier
99 p.identifier = identifier
100 p.valid?
100 p.valid?
101 if valid
101 if valid
102 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
102 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
103 else
103 else
104 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
104 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
105 end
105 end
106 end
106 end
107 end
107 end
108
108
109 def test_identifier_should_not_be_frozen_for_a_new_project
109 def test_identifier_should_not_be_frozen_for_a_new_project
110 assert_equal false, Project.new.identifier_frozen?
110 assert_equal false, Project.new.identifier_frozen?
111 end
111 end
112
112
113 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
113 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
114 Project.update_all(["identifier = ''"], "id = 1")
114 Project.update_all(["identifier = ''"], "id = 1")
115
115
116 assert_equal false, Project.find(1).identifier_frozen?
116 assert_equal false, Project.find(1).identifier_frozen?
117 end
117 end
118
118
119 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
119 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
120 assert_equal true, Project.find(1).identifier_frozen?
120 assert_equal true, Project.find(1).identifier_frozen?
121 end
121 end
122
122
123 def test_members_should_be_active_users
123 def test_members_should_be_active_users
124 Project.all.each do |project|
124 Project.all.each do |project|
125 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
125 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
126 end
126 end
127 end
127 end
128
128
129 def test_users_should_be_active_users
129 def test_users_should_be_active_users
130 Project.all.each do |project|
130 Project.all.each do |project|
131 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
131 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
132 end
132 end
133 end
133 end
134
134
135 def test_open_scope_on_issues_association
136 assert_kind_of Issue, Project.find(1).issues.open.first
137 end
138
135 def test_archive
139 def test_archive
136 user = @ecookbook.members.first.user
140 user = @ecookbook.members.first.user
137 @ecookbook.archive
141 @ecookbook.archive
138 @ecookbook.reload
142 @ecookbook.reload
139
143
140 assert !@ecookbook.active?
144 assert !@ecookbook.active?
141 assert @ecookbook.archived?
145 assert @ecookbook.archived?
142 assert !user.projects.include?(@ecookbook)
146 assert !user.projects.include?(@ecookbook)
143 # Subproject are also archived
147 # Subproject are also archived
144 assert !@ecookbook.children.empty?
148 assert !@ecookbook.children.empty?
145 assert @ecookbook.descendants.active.empty?
149 assert @ecookbook.descendants.active.empty?
146 end
150 end
147
151
148 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
152 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
149 # Assign an issue of a project to a version of a child project
153 # Assign an issue of a project to a version of a child project
150 Issue.find(4).update_attribute :fixed_version_id, 4
154 Issue.find(4).update_attribute :fixed_version_id, 4
151
155
152 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
156 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
153 assert_equal false, @ecookbook.archive
157 assert_equal false, @ecookbook.archive
154 end
158 end
155 @ecookbook.reload
159 @ecookbook.reload
156 assert @ecookbook.active?
160 assert @ecookbook.active?
157 end
161 end
158
162
159 def test_unarchive
163 def test_unarchive
160 user = @ecookbook.members.first.user
164 user = @ecookbook.members.first.user
161 @ecookbook.archive
165 @ecookbook.archive
162 # A subproject of an archived project can not be unarchived
166 # A subproject of an archived project can not be unarchived
163 assert !@ecookbook_sub1.unarchive
167 assert !@ecookbook_sub1.unarchive
164
168
165 # Unarchive project
169 # Unarchive project
166 assert @ecookbook.unarchive
170 assert @ecookbook.unarchive
167 @ecookbook.reload
171 @ecookbook.reload
168 assert @ecookbook.active?
172 assert @ecookbook.active?
169 assert !@ecookbook.archived?
173 assert !@ecookbook.archived?
170 assert user.projects.include?(@ecookbook)
174 assert user.projects.include?(@ecookbook)
171 # Subproject can now be unarchived
175 # Subproject can now be unarchived
172 @ecookbook_sub1.reload
176 @ecookbook_sub1.reload
173 assert @ecookbook_sub1.unarchive
177 assert @ecookbook_sub1.unarchive
174 end
178 end
175
179
176 def test_destroy
180 def test_destroy
177 # 2 active members
181 # 2 active members
178 assert_equal 2, @ecookbook.members.size
182 assert_equal 2, @ecookbook.members.size
179 # and 1 is locked
183 # and 1 is locked
180 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
184 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
181 # some boards
185 # some boards
182 assert @ecookbook.boards.any?
186 assert @ecookbook.boards.any?
183
187
184 @ecookbook.destroy
188 @ecookbook.destroy
185 # make sure that the project non longer exists
189 # make sure that the project non longer exists
186 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
190 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
187 # make sure related data was removed
191 # make sure related data was removed
188 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
192 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
189 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
193 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
190 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
194 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
191 end
195 end
192
196
193 def test_destroy_should_destroy_subtasks
197 def test_destroy_should_destroy_subtasks
194 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
198 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
195 issues[0].update_attribute :parent_issue_id, issues[1].id
199 issues[0].update_attribute :parent_issue_id, issues[1].id
196 issues[2].update_attribute :parent_issue_id, issues[1].id
200 issues[2].update_attribute :parent_issue_id, issues[1].id
197 assert_equal 2, issues[1].children.count
201 assert_equal 2, issues[1].children.count
198
202
199 assert_nothing_raised do
203 assert_nothing_raised do
200 Project.find(1).destroy
204 Project.find(1).destroy
201 end
205 end
202 assert Issue.find_all_by_id(issues.map(&:id)).empty?
206 assert Issue.find_all_by_id(issues.map(&:id)).empty?
203 end
207 end
204
208
205 def test_destroying_root_projects_should_clear_data
209 def test_destroying_root_projects_should_clear_data
206 Project.roots.each do |root|
210 Project.roots.each do |root|
207 root.destroy
211 root.destroy
208 end
212 end
209
213
210 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
214 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
211 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
215 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
212 assert_equal 0, MemberRole.count
216 assert_equal 0, MemberRole.count
213 assert_equal 0, Issue.count
217 assert_equal 0, Issue.count
214 assert_equal 0, Journal.count
218 assert_equal 0, Journal.count
215 assert_equal 0, JournalDetail.count
219 assert_equal 0, JournalDetail.count
216 assert_equal 0, Attachment.count
220 assert_equal 0, Attachment.count
217 assert_equal 0, EnabledModule.count
221 assert_equal 0, EnabledModule.count
218 assert_equal 0, IssueCategory.count
222 assert_equal 0, IssueCategory.count
219 assert_equal 0, IssueRelation.count
223 assert_equal 0, IssueRelation.count
220 assert_equal 0, Board.count
224 assert_equal 0, Board.count
221 assert_equal 0, Message.count
225 assert_equal 0, Message.count
222 assert_equal 0, News.count
226 assert_equal 0, News.count
223 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
227 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
224 assert_equal 0, Repository.count
228 assert_equal 0, Repository.count
225 assert_equal 0, Changeset.count
229 assert_equal 0, Changeset.count
226 assert_equal 0, Change.count
230 assert_equal 0, Change.count
227 assert_equal 0, Comment.count
231 assert_equal 0, Comment.count
228 assert_equal 0, TimeEntry.count
232 assert_equal 0, TimeEntry.count
229 assert_equal 0, Version.count
233 assert_equal 0, Version.count
230 assert_equal 0, Watcher.count
234 assert_equal 0, Watcher.count
231 assert_equal 0, Wiki.count
235 assert_equal 0, Wiki.count
232 assert_equal 0, WikiPage.count
236 assert_equal 0, WikiPage.count
233 assert_equal 0, WikiContent.count
237 assert_equal 0, WikiContent.count
234 assert_equal 0, WikiContent::Version.count
238 assert_equal 0, WikiContent::Version.count
235 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
239 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
236 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
240 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
237 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
241 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
238 end
242 end
239
243
240 def test_move_an_orphan_project_to_a_root_project
244 def test_move_an_orphan_project_to_a_root_project
241 sub = Project.find(2)
245 sub = Project.find(2)
242 sub.set_parent! @ecookbook
246 sub.set_parent! @ecookbook
243 assert_equal @ecookbook.id, sub.parent.id
247 assert_equal @ecookbook.id, sub.parent.id
244 @ecookbook.reload
248 @ecookbook.reload
245 assert_equal 4, @ecookbook.children.size
249 assert_equal 4, @ecookbook.children.size
246 end
250 end
247
251
248 def test_move_an_orphan_project_to_a_subproject
252 def test_move_an_orphan_project_to_a_subproject
249 sub = Project.find(2)
253 sub = Project.find(2)
250 assert sub.set_parent!(@ecookbook_sub1)
254 assert sub.set_parent!(@ecookbook_sub1)
251 end
255 end
252
256
253 def test_move_a_root_project_to_a_project
257 def test_move_a_root_project_to_a_project
254 sub = @ecookbook
258 sub = @ecookbook
255 assert sub.set_parent!(Project.find(2))
259 assert sub.set_parent!(Project.find(2))
256 end
260 end
257
261
258 def test_should_not_move_a_project_to_its_children
262 def test_should_not_move_a_project_to_its_children
259 sub = @ecookbook
263 sub = @ecookbook
260 assert !(sub.set_parent!(Project.find(3)))
264 assert !(sub.set_parent!(Project.find(3)))
261 end
265 end
262
266
263 def test_set_parent_should_add_roots_in_alphabetical_order
267 def test_set_parent_should_add_roots_in_alphabetical_order
264 ProjectCustomField.delete_all
268 ProjectCustomField.delete_all
265 Project.delete_all
269 Project.delete_all
266 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
270 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
267 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
271 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
268 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
272 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
269 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
273 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
270
274
271 assert_equal 4, Project.count
275 assert_equal 4, Project.count
272 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
276 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
273 end
277 end
274
278
275 def test_set_parent_should_add_children_in_alphabetical_order
279 def test_set_parent_should_add_children_in_alphabetical_order
276 ProjectCustomField.delete_all
280 ProjectCustomField.delete_all
277 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
281 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
278 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
282 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
279 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
283 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
280 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
284 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
281 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
285 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
282
286
283 parent.reload
287 parent.reload
284 assert_equal 4, parent.children.size
288 assert_equal 4, parent.children.size
285 assert_equal parent.children.all.sort_by(&:name), parent.children.all
289 assert_equal parent.children.all.sort_by(&:name), parent.children.all
286 end
290 end
287
291
288 def test_rebuild_should_sort_children_alphabetically
292 def test_rebuild_should_sort_children_alphabetically
289 ProjectCustomField.delete_all
293 ProjectCustomField.delete_all
290 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
294 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
291 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
295 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
292 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
296 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
293 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
297 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
294 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
298 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
295
299
296 Project.update_all("lft = NULL, rgt = NULL")
300 Project.update_all("lft = NULL, rgt = NULL")
297 Project.rebuild!
301 Project.rebuild!
298
302
299 parent.reload
303 parent.reload
300 assert_equal 4, parent.children.size
304 assert_equal 4, parent.children.size
301 assert_equal parent.children.all.sort_by(&:name), parent.children.all
305 assert_equal parent.children.all.sort_by(&:name), parent.children.all
302 end
306 end
303
307
304
308
305 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
309 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
306 # Parent issue with a hierarchy project's fixed version
310 # Parent issue with a hierarchy project's fixed version
307 parent_issue = Issue.find(1)
311 parent_issue = Issue.find(1)
308 parent_issue.update_attribute(:fixed_version_id, 4)
312 parent_issue.update_attribute(:fixed_version_id, 4)
309 parent_issue.reload
313 parent_issue.reload
310 assert_equal 4, parent_issue.fixed_version_id
314 assert_equal 4, parent_issue.fixed_version_id
311
315
312 # Should keep fixed versions for the issues
316 # Should keep fixed versions for the issues
313 issue_with_local_fixed_version = Issue.find(5)
317 issue_with_local_fixed_version = Issue.find(5)
314 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
318 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
315 issue_with_local_fixed_version.reload
319 issue_with_local_fixed_version.reload
316 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
320 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
317
321
318 # Local issue with hierarchy fixed_version
322 # Local issue with hierarchy fixed_version
319 issue_with_hierarchy_fixed_version = Issue.find(13)
323 issue_with_hierarchy_fixed_version = Issue.find(13)
320 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
324 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
321 issue_with_hierarchy_fixed_version.reload
325 issue_with_hierarchy_fixed_version.reload
322 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
326 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
323
327
324 # Move project out of the issue's hierarchy
328 # Move project out of the issue's hierarchy
325 moved_project = Project.find(3)
329 moved_project = Project.find(3)
326 moved_project.set_parent!(Project.find(2))
330 moved_project.set_parent!(Project.find(2))
327 parent_issue.reload
331 parent_issue.reload
328 issue_with_local_fixed_version.reload
332 issue_with_local_fixed_version.reload
329 issue_with_hierarchy_fixed_version.reload
333 issue_with_hierarchy_fixed_version.reload
330
334
331 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
335 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
332 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
336 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
333 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
337 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
334 end
338 end
335
339
336 def test_parent
340 def test_parent
337 p = Project.find(6).parent
341 p = Project.find(6).parent
338 assert p.is_a?(Project)
342 assert p.is_a?(Project)
339 assert_equal 5, p.id
343 assert_equal 5, p.id
340 end
344 end
341
345
342 def test_ancestors
346 def test_ancestors
343 a = Project.find(6).ancestors
347 a = Project.find(6).ancestors
344 assert a.first.is_a?(Project)
348 assert a.first.is_a?(Project)
345 assert_equal [1, 5], a.collect(&:id)
349 assert_equal [1, 5], a.collect(&:id)
346 end
350 end
347
351
348 def test_root
352 def test_root
349 r = Project.find(6).root
353 r = Project.find(6).root
350 assert r.is_a?(Project)
354 assert r.is_a?(Project)
351 assert_equal 1, r.id
355 assert_equal 1, r.id
352 end
356 end
353
357
354 def test_children
358 def test_children
355 c = Project.find(1).children
359 c = Project.find(1).children
356 assert c.first.is_a?(Project)
360 assert c.first.is_a?(Project)
357 assert_equal [5, 3, 4], c.collect(&:id)
361 assert_equal [5, 3, 4], c.collect(&:id)
358 end
362 end
359
363
360 def test_descendants
364 def test_descendants
361 d = Project.find(1).descendants
365 d = Project.find(1).descendants
362 assert d.first.is_a?(Project)
366 assert d.first.is_a?(Project)
363 assert_equal [5, 6, 3, 4], d.collect(&:id)
367 assert_equal [5, 6, 3, 4], d.collect(&:id)
364 end
368 end
365
369
366 def test_allowed_parents_should_be_empty_for_non_member_user
370 def test_allowed_parents_should_be_empty_for_non_member_user
367 Role.non_member.add_permission!(:add_project)
371 Role.non_member.add_permission!(:add_project)
368 user = User.find(9)
372 user = User.find(9)
369 assert user.memberships.empty?
373 assert user.memberships.empty?
370 User.current = user
374 User.current = user
371 assert Project.new.allowed_parents.compact.empty?
375 assert Project.new.allowed_parents.compact.empty?
372 end
376 end
373
377
374 def test_allowed_parents_with_add_subprojects_permission
378 def test_allowed_parents_with_add_subprojects_permission
375 Role.find(1).remove_permission!(:add_project)
379 Role.find(1).remove_permission!(:add_project)
376 Role.find(1).add_permission!(:add_subprojects)
380 Role.find(1).add_permission!(:add_subprojects)
377 User.current = User.find(2)
381 User.current = User.find(2)
378 # new project
382 # new project
379 assert !Project.new.allowed_parents.include?(nil)
383 assert !Project.new.allowed_parents.include?(nil)
380 assert Project.new.allowed_parents.include?(Project.find(1))
384 assert Project.new.allowed_parents.include?(Project.find(1))
381 # existing root project
385 # existing root project
382 assert Project.find(1).allowed_parents.include?(nil)
386 assert Project.find(1).allowed_parents.include?(nil)
383 # existing child
387 # existing child
384 assert Project.find(3).allowed_parents.include?(Project.find(1))
388 assert Project.find(3).allowed_parents.include?(Project.find(1))
385 assert !Project.find(3).allowed_parents.include?(nil)
389 assert !Project.find(3).allowed_parents.include?(nil)
386 end
390 end
387
391
388 def test_allowed_parents_with_add_project_permission
392 def test_allowed_parents_with_add_project_permission
389 Role.find(1).add_permission!(:add_project)
393 Role.find(1).add_permission!(:add_project)
390 Role.find(1).remove_permission!(:add_subprojects)
394 Role.find(1).remove_permission!(:add_subprojects)
391 User.current = User.find(2)
395 User.current = User.find(2)
392 # new project
396 # new project
393 assert Project.new.allowed_parents.include?(nil)
397 assert Project.new.allowed_parents.include?(nil)
394 assert !Project.new.allowed_parents.include?(Project.find(1))
398 assert !Project.new.allowed_parents.include?(Project.find(1))
395 # existing root project
399 # existing root project
396 assert Project.find(1).allowed_parents.include?(nil)
400 assert Project.find(1).allowed_parents.include?(nil)
397 # existing child
401 # existing child
398 assert Project.find(3).allowed_parents.include?(Project.find(1))
402 assert Project.find(3).allowed_parents.include?(Project.find(1))
399 assert Project.find(3).allowed_parents.include?(nil)
403 assert Project.find(3).allowed_parents.include?(nil)
400 end
404 end
401
405
402 def test_allowed_parents_with_add_project_and_subprojects_permission
406 def test_allowed_parents_with_add_project_and_subprojects_permission
403 Role.find(1).add_permission!(:add_project)
407 Role.find(1).add_permission!(:add_project)
404 Role.find(1).add_permission!(:add_subprojects)
408 Role.find(1).add_permission!(:add_subprojects)
405 User.current = User.find(2)
409 User.current = User.find(2)
406 # new project
410 # new project
407 assert Project.new.allowed_parents.include?(nil)
411 assert Project.new.allowed_parents.include?(nil)
408 assert Project.new.allowed_parents.include?(Project.find(1))
412 assert Project.new.allowed_parents.include?(Project.find(1))
409 # existing root project
413 # existing root project
410 assert Project.find(1).allowed_parents.include?(nil)
414 assert Project.find(1).allowed_parents.include?(nil)
411 # existing child
415 # existing child
412 assert Project.find(3).allowed_parents.include?(Project.find(1))
416 assert Project.find(3).allowed_parents.include?(Project.find(1))
413 assert Project.find(3).allowed_parents.include?(nil)
417 assert Project.find(3).allowed_parents.include?(nil)
414 end
418 end
415
419
416 def test_users_by_role
420 def test_users_by_role
417 users_by_role = Project.find(1).users_by_role
421 users_by_role = Project.find(1).users_by_role
418 assert_kind_of Hash, users_by_role
422 assert_kind_of Hash, users_by_role
419 role = Role.find(1)
423 role = Role.find(1)
420 assert_kind_of Array, users_by_role[role]
424 assert_kind_of Array, users_by_role[role]
421 assert users_by_role[role].include?(User.find(2))
425 assert users_by_role[role].include?(User.find(2))
422 end
426 end
423
427
424 def test_rolled_up_trackers
428 def test_rolled_up_trackers
425 parent = Project.find(1)
429 parent = Project.find(1)
426 parent.trackers = Tracker.find([1,2])
430 parent.trackers = Tracker.find([1,2])
427 child = parent.children.find(3)
431 child = parent.children.find(3)
428
432
429 assert_equal [1, 2], parent.tracker_ids
433 assert_equal [1, 2], parent.tracker_ids
430 assert_equal [2, 3], child.trackers.collect(&:id)
434 assert_equal [2, 3], child.trackers.collect(&:id)
431
435
432 assert_kind_of Tracker, parent.rolled_up_trackers.first
436 assert_kind_of Tracker, parent.rolled_up_trackers.first
433 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
437 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
434
438
435 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
439 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
436 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
440 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
437 end
441 end
438
442
439 def test_rolled_up_trackers_should_ignore_archived_subprojects
443 def test_rolled_up_trackers_should_ignore_archived_subprojects
440 parent = Project.find(1)
444 parent = Project.find(1)
441 parent.trackers = Tracker.find([1,2])
445 parent.trackers = Tracker.find([1,2])
442 child = parent.children.find(3)
446 child = parent.children.find(3)
443 child.trackers = Tracker.find([1,3])
447 child.trackers = Tracker.find([1,3])
444 parent.children.each(&:archive)
448 parent.children.each(&:archive)
445
449
446 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
450 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
447 end
451 end
448
452
449 context "#rolled_up_versions" do
453 context "#rolled_up_versions" do
450 setup do
454 setup do
451 @project = Project.generate!
455 @project = Project.generate!
452 @parent_version_1 = Version.generate!(:project => @project)
456 @parent_version_1 = Version.generate!(:project => @project)
453 @parent_version_2 = Version.generate!(:project => @project)
457 @parent_version_2 = Version.generate!(:project => @project)
454 end
458 end
455
459
456 should "include the versions for the current project" do
460 should "include the versions for the current project" do
457 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
461 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
458 end
462 end
459
463
460 should "include versions for a subproject" do
464 should "include versions for a subproject" do
461 @subproject = Project.generate!
465 @subproject = Project.generate!
462 @subproject.set_parent!(@project)
466 @subproject.set_parent!(@project)
463 @subproject_version = Version.generate!(:project => @subproject)
467 @subproject_version = Version.generate!(:project => @subproject)
464
468
465 assert_same_elements [
469 assert_same_elements [
466 @parent_version_1,
470 @parent_version_1,
467 @parent_version_2,
471 @parent_version_2,
468 @subproject_version
472 @subproject_version
469 ], @project.rolled_up_versions
473 ], @project.rolled_up_versions
470 end
474 end
471
475
472 should "include versions for a sub-subproject" do
476 should "include versions for a sub-subproject" do
473 @subproject = Project.generate!
477 @subproject = Project.generate!
474 @subproject.set_parent!(@project)
478 @subproject.set_parent!(@project)
475 @sub_subproject = Project.generate!
479 @sub_subproject = Project.generate!
476 @sub_subproject.set_parent!(@subproject)
480 @sub_subproject.set_parent!(@subproject)
477 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
481 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
478
482
479 @project.reload
483 @project.reload
480
484
481 assert_same_elements [
485 assert_same_elements [
482 @parent_version_1,
486 @parent_version_1,
483 @parent_version_2,
487 @parent_version_2,
484 @sub_subproject_version
488 @sub_subproject_version
485 ], @project.rolled_up_versions
489 ], @project.rolled_up_versions
486 end
490 end
487
491
488 should "only check active projects" do
492 should "only check active projects" do
489 @subproject = Project.generate!
493 @subproject = Project.generate!
490 @subproject.set_parent!(@project)
494 @subproject.set_parent!(@project)
491 @subproject_version = Version.generate!(:project => @subproject)
495 @subproject_version = Version.generate!(:project => @subproject)
492 assert @subproject.archive
496 assert @subproject.archive
493
497
494 @project.reload
498 @project.reload
495
499
496 assert !@subproject.active?
500 assert !@subproject.active?
497 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
501 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
498 end
502 end
499 end
503 end
500
504
501 def test_shared_versions_none_sharing
505 def test_shared_versions_none_sharing
502 p = Project.find(5)
506 p = Project.find(5)
503 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
507 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
504 assert p.shared_versions.include?(v)
508 assert p.shared_versions.include?(v)
505 assert !p.children.first.shared_versions.include?(v)
509 assert !p.children.first.shared_versions.include?(v)
506 assert !p.root.shared_versions.include?(v)
510 assert !p.root.shared_versions.include?(v)
507 assert !p.siblings.first.shared_versions.include?(v)
511 assert !p.siblings.first.shared_versions.include?(v)
508 assert !p.root.siblings.first.shared_versions.include?(v)
512 assert !p.root.siblings.first.shared_versions.include?(v)
509 end
513 end
510
514
511 def test_shared_versions_descendants_sharing
515 def test_shared_versions_descendants_sharing
512 p = Project.find(5)
516 p = Project.find(5)
513 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
517 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
514 assert p.shared_versions.include?(v)
518 assert p.shared_versions.include?(v)
515 assert p.children.first.shared_versions.include?(v)
519 assert p.children.first.shared_versions.include?(v)
516 assert !p.root.shared_versions.include?(v)
520 assert !p.root.shared_versions.include?(v)
517 assert !p.siblings.first.shared_versions.include?(v)
521 assert !p.siblings.first.shared_versions.include?(v)
518 assert !p.root.siblings.first.shared_versions.include?(v)
522 assert !p.root.siblings.first.shared_versions.include?(v)
519 end
523 end
520
524
521 def test_shared_versions_hierarchy_sharing
525 def test_shared_versions_hierarchy_sharing
522 p = Project.find(5)
526 p = Project.find(5)
523 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
527 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
524 assert p.shared_versions.include?(v)
528 assert p.shared_versions.include?(v)
525 assert p.children.first.shared_versions.include?(v)
529 assert p.children.first.shared_versions.include?(v)
526 assert p.root.shared_versions.include?(v)
530 assert p.root.shared_versions.include?(v)
527 assert !p.siblings.first.shared_versions.include?(v)
531 assert !p.siblings.first.shared_versions.include?(v)
528 assert !p.root.siblings.first.shared_versions.include?(v)
532 assert !p.root.siblings.first.shared_versions.include?(v)
529 end
533 end
530
534
531 def test_shared_versions_tree_sharing
535 def test_shared_versions_tree_sharing
532 p = Project.find(5)
536 p = Project.find(5)
533 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
537 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
534 assert p.shared_versions.include?(v)
538 assert p.shared_versions.include?(v)
535 assert p.children.first.shared_versions.include?(v)
539 assert p.children.first.shared_versions.include?(v)
536 assert p.root.shared_versions.include?(v)
540 assert p.root.shared_versions.include?(v)
537 assert p.siblings.first.shared_versions.include?(v)
541 assert p.siblings.first.shared_versions.include?(v)
538 assert !p.root.siblings.first.shared_versions.include?(v)
542 assert !p.root.siblings.first.shared_versions.include?(v)
539 end
543 end
540
544
541 def test_shared_versions_system_sharing
545 def test_shared_versions_system_sharing
542 p = Project.find(5)
546 p = Project.find(5)
543 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
547 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
544 assert p.shared_versions.include?(v)
548 assert p.shared_versions.include?(v)
545 assert p.children.first.shared_versions.include?(v)
549 assert p.children.first.shared_versions.include?(v)
546 assert p.root.shared_versions.include?(v)
550 assert p.root.shared_versions.include?(v)
547 assert p.siblings.first.shared_versions.include?(v)
551 assert p.siblings.first.shared_versions.include?(v)
548 assert p.root.siblings.first.shared_versions.include?(v)
552 assert p.root.siblings.first.shared_versions.include?(v)
549 end
553 end
550
554
551 def test_shared_versions
555 def test_shared_versions
552 parent = Project.find(1)
556 parent = Project.find(1)
553 child = parent.children.find(3)
557 child = parent.children.find(3)
554 private_child = parent.children.find(5)
558 private_child = parent.children.find(5)
555
559
556 assert_equal [1,2,3], parent.version_ids.sort
560 assert_equal [1,2,3], parent.version_ids.sort
557 assert_equal [4], child.version_ids
561 assert_equal [4], child.version_ids
558 assert_equal [6], private_child.version_ids
562 assert_equal [6], private_child.version_ids
559 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
563 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
560
564
561 assert_equal 6, parent.shared_versions.size
565 assert_equal 6, parent.shared_versions.size
562 parent.shared_versions.each do |version|
566 parent.shared_versions.each do |version|
563 assert_kind_of Version, version
567 assert_kind_of Version, version
564 end
568 end
565
569
566 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
570 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
567 end
571 end
568
572
569 def test_shared_versions_should_ignore_archived_subprojects
573 def test_shared_versions_should_ignore_archived_subprojects
570 parent = Project.find(1)
574 parent = Project.find(1)
571 child = parent.children.find(3)
575 child = parent.children.find(3)
572 child.archive
576 child.archive
573 parent.reload
577 parent.reload
574
578
575 assert_equal [1,2,3], parent.version_ids.sort
579 assert_equal [1,2,3], parent.version_ids.sort
576 assert_equal [4], child.version_ids
580 assert_equal [4], child.version_ids
577 assert !parent.shared_versions.collect(&:id).include?(4)
581 assert !parent.shared_versions.collect(&:id).include?(4)
578 end
582 end
579
583
580 def test_shared_versions_visible_to_user
584 def test_shared_versions_visible_to_user
581 user = User.find(3)
585 user = User.find(3)
582 parent = Project.find(1)
586 parent = Project.find(1)
583 child = parent.children.find(5)
587 child = parent.children.find(5)
584
588
585 assert_equal [1,2,3], parent.version_ids.sort
589 assert_equal [1,2,3], parent.version_ids.sort
586 assert_equal [6], child.version_ids
590 assert_equal [6], child.version_ids
587
591
588 versions = parent.shared_versions.visible(user)
592 versions = parent.shared_versions.visible(user)
589
593
590 assert_equal 4, versions.size
594 assert_equal 4, versions.size
591 versions.each do |version|
595 versions.each do |version|
592 assert_kind_of Version, version
596 assert_kind_of Version, version
593 end
597 end
594
598
595 assert !versions.collect(&:id).include?(6)
599 assert !versions.collect(&:id).include?(6)
596 end
600 end
597
601
598 def test_shared_versions_for_new_project_should_include_system_shared_versions
602 def test_shared_versions_for_new_project_should_include_system_shared_versions
599 p = Project.find(5)
603 p = Project.find(5)
600 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
604 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
601
605
602 assert_include v, Project.new.shared_versions
606 assert_include v, Project.new.shared_versions
603 end
607 end
604
608
605 def test_next_identifier
609 def test_next_identifier
606 ProjectCustomField.delete_all
610 ProjectCustomField.delete_all
607 Project.create!(:name => 'last', :identifier => 'p2008040')
611 Project.create!(:name => 'last', :identifier => 'p2008040')
608 assert_equal 'p2008041', Project.next_identifier
612 assert_equal 'p2008041', Project.next_identifier
609 end
613 end
610
614
611 def test_next_identifier_first_project
615 def test_next_identifier_first_project
612 Project.delete_all
616 Project.delete_all
613 assert_nil Project.next_identifier
617 assert_nil Project.next_identifier
614 end
618 end
615
619
616 def test_enabled_module_names
620 def test_enabled_module_names
617 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
621 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
618 project = Project.new
622 project = Project.new
619
623
620 project.enabled_module_names = %w(issue_tracking news)
624 project.enabled_module_names = %w(issue_tracking news)
621 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
625 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
622 end
626 end
623 end
627 end
624
628
625 context "enabled_modules" do
629 context "enabled_modules" do
626 setup do
630 setup do
627 @project = Project.find(1)
631 @project = Project.find(1)
628 end
632 end
629
633
630 should "define module by names and preserve ids" do
634 should "define module by names and preserve ids" do
631 # Remove one module
635 # Remove one module
632 modules = @project.enabled_modules.slice(0..-2)
636 modules = @project.enabled_modules.slice(0..-2)
633 assert modules.any?
637 assert modules.any?
634 assert_difference 'EnabledModule.count', -1 do
638 assert_difference 'EnabledModule.count', -1 do
635 @project.enabled_module_names = modules.collect(&:name)
639 @project.enabled_module_names = modules.collect(&:name)
636 end
640 end
637 @project.reload
641 @project.reload
638 # Ids should be preserved
642 # Ids should be preserved
639 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
643 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
640 end
644 end
641
645
642 should "enable a module" do
646 should "enable a module" do
643 @project.enabled_module_names = []
647 @project.enabled_module_names = []
644 @project.reload
648 @project.reload
645 assert_equal [], @project.enabled_module_names
649 assert_equal [], @project.enabled_module_names
646 #with string
650 #with string
647 @project.enable_module!("issue_tracking")
651 @project.enable_module!("issue_tracking")
648 assert_equal ["issue_tracking"], @project.enabled_module_names
652 assert_equal ["issue_tracking"], @project.enabled_module_names
649 #with symbol
653 #with symbol
650 @project.enable_module!(:gantt)
654 @project.enable_module!(:gantt)
651 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
655 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
652 #don't add a module twice
656 #don't add a module twice
653 @project.enable_module!("issue_tracking")
657 @project.enable_module!("issue_tracking")
654 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
658 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
655 end
659 end
656
660
657 should "disable a module" do
661 should "disable a module" do
658 #with string
662 #with string
659 assert @project.enabled_module_names.include?("issue_tracking")
663 assert @project.enabled_module_names.include?("issue_tracking")
660 @project.disable_module!("issue_tracking")
664 @project.disable_module!("issue_tracking")
661 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
665 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
662 #with symbol
666 #with symbol
663 assert @project.enabled_module_names.include?("gantt")
667 assert @project.enabled_module_names.include?("gantt")
664 @project.disable_module!(:gantt)
668 @project.disable_module!(:gantt)
665 assert ! @project.reload.enabled_module_names.include?("gantt")
669 assert ! @project.reload.enabled_module_names.include?("gantt")
666 #with EnabledModule object
670 #with EnabledModule object
667 first_module = @project.enabled_modules.first
671 first_module = @project.enabled_modules.first
668 @project.disable_module!(first_module)
672 @project.disable_module!(first_module)
669 assert ! @project.reload.enabled_module_names.include?(first_module.name)
673 assert ! @project.reload.enabled_module_names.include?(first_module.name)
670 end
674 end
671 end
675 end
672
676
673 def test_enabled_module_names_should_not_recreate_enabled_modules
677 def test_enabled_module_names_should_not_recreate_enabled_modules
674 project = Project.find(1)
678 project = Project.find(1)
675 # Remove one module
679 # Remove one module
676 modules = project.enabled_modules.slice(0..-2)
680 modules = project.enabled_modules.slice(0..-2)
677 assert modules.any?
681 assert modules.any?
678 assert_difference 'EnabledModule.count', -1 do
682 assert_difference 'EnabledModule.count', -1 do
679 project.enabled_module_names = modules.collect(&:name)
683 project.enabled_module_names = modules.collect(&:name)
680 end
684 end
681 project.reload
685 project.reload
682 # Ids should be preserved
686 # Ids should be preserved
683 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
687 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
684 end
688 end
685
689
686 def test_copy_from_existing_project
690 def test_copy_from_existing_project
687 source_project = Project.find(1)
691 source_project = Project.find(1)
688 copied_project = Project.copy_from(1)
692 copied_project = Project.copy_from(1)
689
693
690 assert copied_project
694 assert copied_project
691 # Cleared attributes
695 # Cleared attributes
692 assert copied_project.id.blank?
696 assert copied_project.id.blank?
693 assert copied_project.name.blank?
697 assert copied_project.name.blank?
694 assert copied_project.identifier.blank?
698 assert copied_project.identifier.blank?
695
699
696 # Duplicated attributes
700 # Duplicated attributes
697 assert_equal source_project.description, copied_project.description
701 assert_equal source_project.description, copied_project.description
698 assert_equal source_project.enabled_modules, copied_project.enabled_modules
702 assert_equal source_project.enabled_modules, copied_project.enabled_modules
699 assert_equal source_project.trackers, copied_project.trackers
703 assert_equal source_project.trackers, copied_project.trackers
700
704
701 # Default attributes
705 # Default attributes
702 assert_equal 1, copied_project.status
706 assert_equal 1, copied_project.status
703 end
707 end
704
708
705 def test_activities_should_use_the_system_activities
709 def test_activities_should_use_the_system_activities
706 project = Project.find(1)
710 project = Project.find(1)
707 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
711 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
708 end
712 end
709
713
710
714
711 def test_activities_should_use_the_project_specific_activities
715 def test_activities_should_use_the_project_specific_activities
712 project = Project.find(1)
716 project = Project.find(1)
713 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
717 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
714 assert overridden_activity.save!
718 assert overridden_activity.save!
715
719
716 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
720 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
717 end
721 end
718
722
719 def test_activities_should_not_include_the_inactive_project_specific_activities
723 def test_activities_should_not_include_the_inactive_project_specific_activities
720 project = Project.find(1)
724 project = Project.find(1)
721 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
725 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
722 assert overridden_activity.save!
726 assert overridden_activity.save!
723
727
724 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
728 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
725 end
729 end
726
730
727 def test_activities_should_not_include_project_specific_activities_from_other_projects
731 def test_activities_should_not_include_project_specific_activities_from_other_projects
728 project = Project.find(1)
732 project = Project.find(1)
729 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
733 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
730 assert overridden_activity.save!
734 assert overridden_activity.save!
731
735
732 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
736 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
733 end
737 end
734
738
735 def test_activities_should_handle_nils
739 def test_activities_should_handle_nils
736 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
740 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
737 TimeEntryActivity.delete_all
741 TimeEntryActivity.delete_all
738
742
739 # No activities
743 # No activities
740 project = Project.find(1)
744 project = Project.find(1)
741 assert project.activities.empty?
745 assert project.activities.empty?
742
746
743 # No system, one overridden
747 # No system, one overridden
744 assert overridden_activity.save!
748 assert overridden_activity.save!
745 project.reload
749 project.reload
746 assert_equal [overridden_activity], project.activities
750 assert_equal [overridden_activity], project.activities
747 end
751 end
748
752
749 def test_activities_should_override_system_activities_with_project_activities
753 def test_activities_should_override_system_activities_with_project_activities
750 project = Project.find(1)
754 project = Project.find(1)
751 parent_activity = TimeEntryActivity.find(:first)
755 parent_activity = TimeEntryActivity.find(:first)
752 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
756 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
753 assert overridden_activity.save!
757 assert overridden_activity.save!
754
758
755 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
759 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
756 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
760 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
757 end
761 end
758
762
759 def test_activities_should_include_inactive_activities_if_specified
763 def test_activities_should_include_inactive_activities_if_specified
760 project = Project.find(1)
764 project = Project.find(1)
761 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
765 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
762 assert overridden_activity.save!
766 assert overridden_activity.save!
763
767
764 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
768 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
765 end
769 end
766
770
767 test 'activities should not include active System activities if the project has an override that is inactive' do
771 test 'activities should not include active System activities if the project has an override that is inactive' do
768 project = Project.find(1)
772 project = Project.find(1)
769 system_activity = TimeEntryActivity.find_by_name('Design')
773 system_activity = TimeEntryActivity.find_by_name('Design')
770 assert system_activity.active?
774 assert system_activity.active?
771 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
775 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
772 assert overridden_activity.save!
776 assert overridden_activity.save!
773
777
774 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
778 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
775 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
779 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
776 end
780 end
777
781
778 def test_close_completed_versions
782 def test_close_completed_versions
779 Version.update_all("status = 'open'")
783 Version.update_all("status = 'open'")
780 project = Project.find(1)
784 project = Project.find(1)
781 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
785 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
782 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
786 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
783 project.close_completed_versions
787 project.close_completed_versions
784 project.reload
788 project.reload
785 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
789 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
786 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
790 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
787 end
791 end
788
792
789 context "Project#copy" do
793 context "Project#copy" do
790 setup do
794 setup do
791 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
795 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
792 Project.destroy_all :identifier => "copy-test"
796 Project.destroy_all :identifier => "copy-test"
793 @source_project = Project.find(2)
797 @source_project = Project.find(2)
794 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
798 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
795 @project.trackers = @source_project.trackers
799 @project.trackers = @source_project.trackers
796 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
800 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
797 end
801 end
798
802
799 should "copy issues" do
803 should "copy issues" do
800 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
804 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
801 :subject => "copy issue status",
805 :subject => "copy issue status",
802 :tracker_id => 1,
806 :tracker_id => 1,
803 :assigned_to_id => 2,
807 :assigned_to_id => 2,
804 :project_id => @source_project.id)
808 :project_id => @source_project.id)
805 assert @project.valid?
809 assert @project.valid?
806 assert @project.issues.empty?
810 assert @project.issues.empty?
807 assert @project.copy(@source_project)
811 assert @project.copy(@source_project)
808
812
809 assert_equal @source_project.issues.size, @project.issues.size
813 assert_equal @source_project.issues.size, @project.issues.size
810 @project.issues.each do |issue|
814 @project.issues.each do |issue|
811 assert issue.valid?
815 assert issue.valid?
812 assert ! issue.assigned_to.blank?
816 assert ! issue.assigned_to.blank?
813 assert_equal @project, issue.project
817 assert_equal @project, issue.project
814 end
818 end
815
819
816 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
820 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
817 assert copied_issue
821 assert copied_issue
818 assert copied_issue.status
822 assert copied_issue.status
819 assert_equal "Closed", copied_issue.status.name
823 assert_equal "Closed", copied_issue.status.name
820 end
824 end
821
825
822 should "change the new issues to use the copied version" do
826 should "change the new issues to use the copied version" do
823 User.current = User.find(1)
827 User.current = User.find(1)
824 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
828 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
825 @source_project.versions << assigned_version
829 @source_project.versions << assigned_version
826 assert_equal 3, @source_project.versions.size
830 assert_equal 3, @source_project.versions.size
827 Issue.generate_for_project!(@source_project,
831 Issue.generate_for_project!(@source_project,
828 :fixed_version_id => assigned_version.id,
832 :fixed_version_id => assigned_version.id,
829 :subject => "change the new issues to use the copied version",
833 :subject => "change the new issues to use the copied version",
830 :tracker_id => 1,
834 :tracker_id => 1,
831 :project_id => @source_project.id)
835 :project_id => @source_project.id)
832
836
833 assert @project.copy(@source_project)
837 assert @project.copy(@source_project)
834 @project.reload
838 @project.reload
835 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
839 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
836
840
837 assert copied_issue
841 assert copied_issue
838 assert copied_issue.fixed_version
842 assert copied_issue.fixed_version
839 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
843 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
840 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
844 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
841 end
845 end
842
846
843 should "copy issue relations" do
847 should "copy issue relations" do
844 Setting.cross_project_issue_relations = '1'
848 Setting.cross_project_issue_relations = '1'
845
849
846 second_issue = Issue.generate!(:status_id => 5,
850 second_issue = Issue.generate!(:status_id => 5,
847 :subject => "copy issue relation",
851 :subject => "copy issue relation",
848 :tracker_id => 1,
852 :tracker_id => 1,
849 :assigned_to_id => 2,
853 :assigned_to_id => 2,
850 :project_id => @source_project.id)
854 :project_id => @source_project.id)
851 source_relation = IssueRelation.create!(:issue_from => Issue.find(4),
855 source_relation = IssueRelation.create!(:issue_from => Issue.find(4),
852 :issue_to => second_issue,
856 :issue_to => second_issue,
853 :relation_type => "relates")
857 :relation_type => "relates")
854 source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1),
858 source_relation_cross_project = IssueRelation.create!(:issue_from => Issue.find(1),
855 :issue_to => second_issue,
859 :issue_to => second_issue,
856 :relation_type => "duplicates")
860 :relation_type => "duplicates")
857
861
858 assert @project.copy(@source_project)
862 assert @project.copy(@source_project)
859 assert_equal @source_project.issues.count, @project.issues.count
863 assert_equal @source_project.issues.count, @project.issues.count
860 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
864 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
861 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
865 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
862
866
863 # First issue with a relation on project
867 # First issue with a relation on project
864 assert_equal 1, copied_issue.relations.size, "Relation not copied"
868 assert_equal 1, copied_issue.relations.size, "Relation not copied"
865 copied_relation = copied_issue.relations.first
869 copied_relation = copied_issue.relations.first
866 assert_equal "relates", copied_relation.relation_type
870 assert_equal "relates", copied_relation.relation_type
867 assert_equal copied_second_issue.id, copied_relation.issue_to_id
871 assert_equal copied_second_issue.id, copied_relation.issue_to_id
868 assert_not_equal source_relation.id, copied_relation.id
872 assert_not_equal source_relation.id, copied_relation.id
869
873
870 # Second issue with a cross project relation
874 # Second issue with a cross project relation
871 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
875 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
872 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
876 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
873 assert_equal "duplicates", copied_relation.relation_type
877 assert_equal "duplicates", copied_relation.relation_type
874 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
878 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
875 assert_not_equal source_relation_cross_project.id, copied_relation.id
879 assert_not_equal source_relation_cross_project.id, copied_relation.id
876 end
880 end
877
881
878 should "copy issue attachments" do
882 should "copy issue attachments" do
879 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
883 issue = Issue.generate!(:subject => "copy with attachment", :tracker_id => 1, :project_id => @source_project.id)
880 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
884 Attachment.create!(:container => issue, :file => uploaded_test_file("testfile.txt", "text/plain"), :author_id => 1)
881 @source_project.issues << issue
885 @source_project.issues << issue
882 assert @project.copy(@source_project)
886 assert @project.copy(@source_project)
883
887
884 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
888 copied_issue = @project.issues.first(:conditions => {:subject => "copy with attachment"})
885 assert_not_nil copied_issue
889 assert_not_nil copied_issue
886 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
890 assert_equal 1, copied_issue.attachments.count, "Attachment not copied"
887 assert_equal "testfile.txt", copied_issue.attachments.first.filename
891 assert_equal "testfile.txt", copied_issue.attachments.first.filename
888 end
892 end
889
893
890 should "copy memberships" do
894 should "copy memberships" do
891 assert @project.valid?
895 assert @project.valid?
892 assert @project.members.empty?
896 assert @project.members.empty?
893 assert @project.copy(@source_project)
897 assert @project.copy(@source_project)
894
898
895 assert_equal @source_project.memberships.size, @project.memberships.size
899 assert_equal @source_project.memberships.size, @project.memberships.size
896 @project.memberships.each do |membership|
900 @project.memberships.each do |membership|
897 assert membership
901 assert membership
898 assert_equal @project, membership.project
902 assert_equal @project, membership.project
899 end
903 end
900 end
904 end
901
905
902 should "copy memberships with groups and additional roles" do
906 should "copy memberships with groups and additional roles" do
903 group = Group.create!(:lastname => "Copy group")
907 group = Group.create!(:lastname => "Copy group")
904 user = User.find(7)
908 user = User.find(7)
905 group.users << user
909 group.users << user
906 # group role
910 # group role
907 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
911 Member.create!(:project_id => @source_project.id, :principal => group, :role_ids => [2])
908 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
912 member = Member.find_by_user_id_and_project_id(user.id, @source_project.id)
909 # additional role
913 # additional role
910 member.role_ids = [1]
914 member.role_ids = [1]
911
915
912 assert @project.copy(@source_project)
916 assert @project.copy(@source_project)
913 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
917 member = Member.find_by_user_id_and_project_id(user.id, @project.id)
914 assert_not_nil member
918 assert_not_nil member
915 assert_equal [1, 2], member.role_ids.sort
919 assert_equal [1, 2], member.role_ids.sort
916 end
920 end
917
921
918 should "copy project specific queries" do
922 should "copy project specific queries" do
919 assert @project.valid?
923 assert @project.valid?
920 assert @project.queries.empty?
924 assert @project.queries.empty?
921 assert @project.copy(@source_project)
925 assert @project.copy(@source_project)
922
926
923 assert_equal @source_project.queries.size, @project.queries.size
927 assert_equal @source_project.queries.size, @project.queries.size
924 @project.queries.each do |query|
928 @project.queries.each do |query|
925 assert query
929 assert query
926 assert_equal @project, query.project
930 assert_equal @project, query.project
927 end
931 end
928 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
932 assert_equal @source_project.queries.map(&:user_id).sort, @project.queries.map(&:user_id).sort
929 end
933 end
930
934
931 should "copy versions" do
935 should "copy versions" do
932 @source_project.versions << Version.generate!
936 @source_project.versions << Version.generate!
933 @source_project.versions << Version.generate!
937 @source_project.versions << Version.generate!
934
938
935 assert @project.versions.empty?
939 assert @project.versions.empty?
936 assert @project.copy(@source_project)
940 assert @project.copy(@source_project)
937
941
938 assert_equal @source_project.versions.size, @project.versions.size
942 assert_equal @source_project.versions.size, @project.versions.size
939 @project.versions.each do |version|
943 @project.versions.each do |version|
940 assert version
944 assert version
941 assert_equal @project, version.project
945 assert_equal @project, version.project
942 end
946 end
943 end
947 end
944
948
945 should "copy wiki" do
949 should "copy wiki" do
946 assert_difference 'Wiki.count' do
950 assert_difference 'Wiki.count' do
947 assert @project.copy(@source_project)
951 assert @project.copy(@source_project)
948 end
952 end
949
953
950 assert @project.wiki
954 assert @project.wiki
951 assert_not_equal @source_project.wiki, @project.wiki
955 assert_not_equal @source_project.wiki, @project.wiki
952 assert_equal "Start page", @project.wiki.start_page
956 assert_equal "Start page", @project.wiki.start_page
953 end
957 end
954
958
955 should "copy wiki pages and content with hierarchy" do
959 should "copy wiki pages and content with hierarchy" do
956 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
960 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
957 assert @project.copy(@source_project)
961 assert @project.copy(@source_project)
958 end
962 end
959
963
960 assert @project.wiki
964 assert @project.wiki
961 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
965 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
962
966
963 @project.wiki.pages.each do |wiki_page|
967 @project.wiki.pages.each do |wiki_page|
964 assert wiki_page.content
968 assert wiki_page.content
965 assert !@source_project.wiki.pages.include?(wiki_page)
969 assert !@source_project.wiki.pages.include?(wiki_page)
966 end
970 end
967
971
968 parent = @project.wiki.find_page('Parent_page')
972 parent = @project.wiki.find_page('Parent_page')
969 child1 = @project.wiki.find_page('Child_page_1')
973 child1 = @project.wiki.find_page('Child_page_1')
970 child2 = @project.wiki.find_page('Child_page_2')
974 child2 = @project.wiki.find_page('Child_page_2')
971 assert_equal parent, child1.parent
975 assert_equal parent, child1.parent
972 assert_equal parent, child2.parent
976 assert_equal parent, child2.parent
973 end
977 end
974
978
975 should "copy issue categories" do
979 should "copy issue categories" do
976 assert @project.copy(@source_project)
980 assert @project.copy(@source_project)
977
981
978 assert_equal 2, @project.issue_categories.size
982 assert_equal 2, @project.issue_categories.size
979 @project.issue_categories.each do |issue_category|
983 @project.issue_categories.each do |issue_category|
980 assert !@source_project.issue_categories.include?(issue_category)
984 assert !@source_project.issue_categories.include?(issue_category)
981 end
985 end
982 end
986 end
983
987
984 should "copy boards" do
988 should "copy boards" do
985 assert @project.copy(@source_project)
989 assert @project.copy(@source_project)
986
990
987 assert_equal 1, @project.boards.size
991 assert_equal 1, @project.boards.size
988 @project.boards.each do |board|
992 @project.boards.each do |board|
989 assert !@source_project.boards.include?(board)
993 assert !@source_project.boards.include?(board)
990 end
994 end
991 end
995 end
992
996
993 should "change the new issues to use the copied issue categories" do
997 should "change the new issues to use the copied issue categories" do
994 issue = Issue.find(4)
998 issue = Issue.find(4)
995 issue.update_attribute(:category_id, 3)
999 issue.update_attribute(:category_id, 3)
996
1000
997 assert @project.copy(@source_project)
1001 assert @project.copy(@source_project)
998
1002
999 @project.issues.each do |issue|
1003 @project.issues.each do |issue|
1000 assert issue.category
1004 assert issue.category
1001 assert_equal "Stock management", issue.category.name # Same name
1005 assert_equal "Stock management", issue.category.name # Same name
1002 assert_not_equal IssueCategory.find(3), issue.category # Different record
1006 assert_not_equal IssueCategory.find(3), issue.category # Different record
1003 end
1007 end
1004 end
1008 end
1005
1009
1006 should "limit copy with :only option" do
1010 should "limit copy with :only option" do
1007 assert @project.members.empty?
1011 assert @project.members.empty?
1008 assert @project.issue_categories.empty?
1012 assert @project.issue_categories.empty?
1009 assert @source_project.issues.any?
1013 assert @source_project.issues.any?
1010
1014
1011 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
1015 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
1012
1016
1013 assert @project.members.any?
1017 assert @project.members.any?
1014 assert @project.issue_categories.any?
1018 assert @project.issue_categories.any?
1015 assert @project.issues.empty?
1019 assert @project.issues.empty?
1016 end
1020 end
1017
1021
1018 end
1022 end
1019
1023
1020 context "#start_date" do
1024 context "#start_date" do
1021 setup do
1025 setup do
1022 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1026 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1023 @project = Project.generate!(:identifier => 'test0')
1027 @project = Project.generate!(:identifier => 'test0')
1024 @project.trackers << Tracker.generate!
1028 @project.trackers << Tracker.generate!
1025 end
1029 end
1026
1030
1027 should "be nil if there are no issues on the project" do
1031 should "be nil if there are no issues on the project" do
1028 assert_nil @project.start_date
1032 assert_nil @project.start_date
1029 end
1033 end
1030
1034
1031 should "be tested when issues have no start date"
1035 should "be tested when issues have no start date"
1032
1036
1033 should "be the earliest start date of it's issues" do
1037 should "be the earliest start date of it's issues" do
1034 early = 7.days.ago.to_date
1038 early = 7.days.ago.to_date
1035 Issue.generate_for_project!(@project, :start_date => Date.today)
1039 Issue.generate_for_project!(@project, :start_date => Date.today)
1036 Issue.generate_for_project!(@project, :start_date => early)
1040 Issue.generate_for_project!(@project, :start_date => early)
1037
1041
1038 assert_equal early, @project.start_date
1042 assert_equal early, @project.start_date
1039 end
1043 end
1040
1044
1041 end
1045 end
1042
1046
1043 context "#due_date" do
1047 context "#due_date" do
1044 setup do
1048 setup do
1045 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1049 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1046 @project = Project.generate!(:identifier => 'test0')
1050 @project = Project.generate!(:identifier => 'test0')
1047 @project.trackers << Tracker.generate!
1051 @project.trackers << Tracker.generate!
1048 end
1052 end
1049
1053
1050 should "be nil if there are no issues on the project" do
1054 should "be nil if there are no issues on the project" do
1051 assert_nil @project.due_date
1055 assert_nil @project.due_date
1052 end
1056 end
1053
1057
1054 should "be tested when issues have no due date"
1058 should "be tested when issues have no due date"
1055
1059
1056 should "be the latest due date of it's issues" do
1060 should "be the latest due date of it's issues" do
1057 future = 7.days.from_now.to_date
1061 future = 7.days.from_now.to_date
1058 Issue.generate_for_project!(@project, :due_date => future)
1062 Issue.generate_for_project!(@project, :due_date => future)
1059 Issue.generate_for_project!(@project, :due_date => Date.today)
1063 Issue.generate_for_project!(@project, :due_date => Date.today)
1060
1064
1061 assert_equal future, @project.due_date
1065 assert_equal future, @project.due_date
1062 end
1066 end
1063
1067
1064 should "be the latest due date of it's versions" do
1068 should "be the latest due date of it's versions" do
1065 future = 7.days.from_now.to_date
1069 future = 7.days.from_now.to_date
1066 @project.versions << Version.generate!(:effective_date => future)
1070 @project.versions << Version.generate!(:effective_date => future)
1067 @project.versions << Version.generate!(:effective_date => Date.today)
1071 @project.versions << Version.generate!(:effective_date => Date.today)
1068
1072
1069
1073
1070 assert_equal future, @project.due_date
1074 assert_equal future, @project.due_date
1071
1075
1072 end
1076 end
1073
1077
1074 should "pick the latest date from it's issues and versions" do
1078 should "pick the latest date from it's issues and versions" do
1075 future = 7.days.from_now.to_date
1079 future = 7.days.from_now.to_date
1076 far_future = 14.days.from_now.to_date
1080 far_future = 14.days.from_now.to_date
1077 Issue.generate_for_project!(@project, :due_date => far_future)
1081 Issue.generate_for_project!(@project, :due_date => far_future)
1078 @project.versions << Version.generate!(:effective_date => future)
1082 @project.versions << Version.generate!(:effective_date => future)
1079
1083
1080 assert_equal far_future, @project.due_date
1084 assert_equal far_future, @project.due_date
1081 end
1085 end
1082
1086
1083 end
1087 end
1084
1088
1085 context "Project#completed_percent" do
1089 context "Project#completed_percent" do
1086 setup do
1090 setup do
1087 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1091 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
1088 @project = Project.generate!(:identifier => 'test0')
1092 @project = Project.generate!(:identifier => 'test0')
1089 @project.trackers << Tracker.generate!
1093 @project.trackers << Tracker.generate!
1090 end
1094 end
1091
1095
1092 context "no versions" do
1096 context "no versions" do
1093 should "be 100" do
1097 should "be 100" do
1094 assert_equal 100, @project.completed_percent
1098 assert_equal 100, @project.completed_percent
1095 end
1099 end
1096 end
1100 end
1097
1101
1098 context "with versions" do
1102 context "with versions" do
1099 should "return 0 if the versions have no issues" do
1103 should "return 0 if the versions have no issues" do
1100 Version.generate!(:project => @project)
1104 Version.generate!(:project => @project)
1101 Version.generate!(:project => @project)
1105 Version.generate!(:project => @project)
1102
1106
1103 assert_equal 0, @project.completed_percent
1107 assert_equal 0, @project.completed_percent
1104 end
1108 end
1105
1109
1106 should "return 100 if the version has only closed issues" do
1110 should "return 100 if the version has only closed issues" do
1107 v1 = Version.generate!(:project => @project)
1111 v1 = Version.generate!(:project => @project)
1108 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1112 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
1109 v2 = Version.generate!(:project => @project)
1113 v2 = Version.generate!(:project => @project)
1110 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1114 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
1111
1115
1112 assert_equal 100, @project.completed_percent
1116 assert_equal 100, @project.completed_percent
1113 end
1117 end
1114
1118
1115 should "return the averaged completed percent of the versions (not weighted)" do
1119 should "return the averaged completed percent of the versions (not weighted)" do
1116 v1 = Version.generate!(:project => @project)
1120 v1 = Version.generate!(:project => @project)
1117 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1121 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
1118 v2 = Version.generate!(:project => @project)
1122 v2 = Version.generate!(:project => @project)
1119 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1123 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
1120
1124
1121 assert_equal 50, @project.completed_percent
1125 assert_equal 50, @project.completed_percent
1122 end
1126 end
1123
1127
1124 end
1128 end
1125 end
1129 end
1126
1130
1127 context "#notified_users" do
1131 context "#notified_users" do
1128 setup do
1132 setup do
1129 @project = Project.generate!
1133 @project = Project.generate!
1130 @role = Role.generate!
1134 @role = Role.generate!
1131
1135
1132 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1136 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1133 Member.create!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1137 Member.create!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1134
1138
1135 @all_events_user = User.generate!(:mail_notification => 'all')
1139 @all_events_user = User.generate!(:mail_notification => 'all')
1136 Member.create!(:project => @project, :roles => [@role], :principal => @all_events_user)
1140 Member.create!(:project => @project, :roles => [@role], :principal => @all_events_user)
1137
1141
1138 @no_events_user = User.generate!(:mail_notification => 'none')
1142 @no_events_user = User.generate!(:mail_notification => 'none')
1139 Member.create!(:project => @project, :roles => [@role], :principal => @no_events_user)
1143 Member.create!(:project => @project, :roles => [@role], :principal => @no_events_user)
1140
1144
1141 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1145 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1142 Member.create!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1146 Member.create!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1143
1147
1144 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1148 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1145 Member.create!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1149 Member.create!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1146
1150
1147 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1151 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1148 Member.create!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1152 Member.create!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1149 end
1153 end
1150
1154
1151 should "include members with a mail notification" do
1155 should "include members with a mail notification" do
1152 assert @project.notified_users.include?(@user_with_membership_notification)
1156 assert @project.notified_users.include?(@user_with_membership_notification)
1153 end
1157 end
1154
1158
1155 should "include users with the 'all' notification option" do
1159 should "include users with the 'all' notification option" do
1156 assert @project.notified_users.include?(@all_events_user)
1160 assert @project.notified_users.include?(@all_events_user)
1157 end
1161 end
1158
1162
1159 should "not include users with the 'none' notification option" do
1163 should "not include users with the 'none' notification option" do
1160 assert !@project.notified_users.include?(@no_events_user)
1164 assert !@project.notified_users.include?(@no_events_user)
1161 end
1165 end
1162
1166
1163 should "not include users with the 'only_my_events' notification option" do
1167 should "not include users with the 'only_my_events' notification option" do
1164 assert !@project.notified_users.include?(@only_my_events_user)
1168 assert !@project.notified_users.include?(@only_my_events_user)
1165 end
1169 end
1166
1170
1167 should "not include users with the 'only_assigned' notification option" do
1171 should "not include users with the 'only_assigned' notification option" do
1168 assert !@project.notified_users.include?(@only_assigned_user)
1172 assert !@project.notified_users.include?(@only_assigned_user)
1169 end
1173 end
1170
1174
1171 should "not include users with the 'only_owner' notification option" do
1175 should "not include users with the 'only_owner' notification option" do
1172 assert !@project.notified_users.include?(@only_owned_user)
1176 assert !@project.notified_users.include?(@only_owned_user)
1173 end
1177 end
1174 end
1178 end
1175
1179
1176 end
1180 end
General Comments 0
You need to be logged in to leave comments. Login now