##// END OF EJS Templates
Do not notify users that are no longer allowed to view an issue (#3589, #4263)....
Jean-Philippe Lang -
r3007:c870a7b9ef35
parent child
Show More
@@ -1,356 +1,366
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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 belongs_to :project
19 belongs_to :project
20 belongs_to :tracker
20 belongs_to :tracker
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27
27
28 has_many :journals, :as => :journalized, :dependent => :destroy
28 has_many :journals, :as => :journalized, :dependent => :destroy
29 has_many :time_entries, :dependent => :delete_all
29 has_many :time_entries, :dependent => :delete_all
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31
31
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34
34
35 acts_as_attachable :after_remove => :attachment_removed
35 acts_as_attachable :after_remove => :attachment_removed
36 acts_as_customizable
36 acts_as_customizable
37 acts_as_watchable
37 acts_as_watchable
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 :include => [:project, :journals],
39 :include => [:project, :journals],
40 # sort by id so that limited eager loading doesn't break with postgresql
40 # sort by id so that limited eager loading doesn't break with postgresql
41 :order_column => "#{table_name}.id"
41 :order_column => "#{table_name}.id"
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45
45
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 :author_key => :author_id
47 :author_key => :author_id
48
48
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
49 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
50 validates_length_of :subject, :maximum => 255
50 validates_length_of :subject, :maximum => 255
51 validates_inclusion_of :done_ratio, :in => 0..100
51 validates_inclusion_of :done_ratio, :in => 0..100
52 validates_numericality_of :estimated_hours, :allow_nil => true
52 validates_numericality_of :estimated_hours, :allow_nil => true
53
53
54 named_scope :visible, lambda {|*args| { :include => :project,
54 named_scope :visible, lambda {|*args| { :include => :project,
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
55 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
56
56
57 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
57 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
58
58
59 after_save :create_journal
59 after_save :create_journal
60
60
61 # Returns true if usr or current user is allowed to view the issue
61 # Returns true if usr or current user is allowed to view the issue
62 def visible?(usr=nil)
62 def visible?(usr=nil)
63 (usr || User.current).allowed_to?(:view_issues, self.project)
63 (usr || User.current).allowed_to?(:view_issues, self.project)
64 end
64 end
65
65
66 def after_initialize
66 def after_initialize
67 if new_record?
67 if new_record?
68 # set default values for new records only
68 # set default values for new records only
69 self.status ||= IssueStatus.default
69 self.status ||= IssueStatus.default
70 self.priority ||= IssuePriority.default
70 self.priority ||= IssuePriority.default
71 end
71 end
72 end
72 end
73
73
74 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
74 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
75 def available_custom_fields
75 def available_custom_fields
76 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
76 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
77 end
77 end
78
78
79 def copy_from(arg)
79 def copy_from(arg)
80 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
80 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
81 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
81 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
82 self.custom_values = issue.custom_values.collect {|v| v.clone}
82 self.custom_values = issue.custom_values.collect {|v| v.clone}
83 self.status = issue.status
83 self.status = issue.status
84 self
84 self
85 end
85 end
86
86
87 # Moves/copies an issue to a new project and tracker
87 # Moves/copies an issue to a new project and tracker
88 # Returns the moved/copied issue on success, false on failure
88 # Returns the moved/copied issue on success, false on failure
89 def move_to(new_project, new_tracker = nil, options = {})
89 def move_to(new_project, new_tracker = nil, options = {})
90 options ||= {}
90 options ||= {}
91 issue = options[:copy] ? self.clone : self
91 issue = options[:copy] ? self.clone : self
92 transaction do
92 transaction do
93 if new_project && issue.project_id != new_project.id
93 if new_project && issue.project_id != new_project.id
94 # delete issue relations
94 # delete issue relations
95 unless Setting.cross_project_issue_relations?
95 unless Setting.cross_project_issue_relations?
96 issue.relations_from.clear
96 issue.relations_from.clear
97 issue.relations_to.clear
97 issue.relations_to.clear
98 end
98 end
99 # issue is moved to another project
99 # issue is moved to another project
100 # reassign to the category with same name if any
100 # reassign to the category with same name if any
101 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
101 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
102 issue.category = new_category
102 issue.category = new_category
103 issue.fixed_version = nil
103 issue.fixed_version = nil
104 issue.project = new_project
104 issue.project = new_project
105 end
105 end
106 if new_tracker
106 if new_tracker
107 issue.tracker = new_tracker
107 issue.tracker = new_tracker
108 end
108 end
109 if options[:copy]
109 if options[:copy]
110 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
110 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
111 issue.status = self.status
111 issue.status = self.status
112 end
112 end
113 if issue.save
113 if issue.save
114 unless options[:copy]
114 unless options[:copy]
115 # Manually update project_id on related time entries
115 # Manually update project_id on related time entries
116 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
116 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
117 end
117 end
118 else
118 else
119 Issue.connection.rollback_db_transaction
119 Issue.connection.rollback_db_transaction
120 return false
120 return false
121 end
121 end
122 end
122 end
123 return issue
123 return issue
124 end
124 end
125
125
126 def priority_id=(pid)
126 def priority_id=(pid)
127 self.priority = nil
127 self.priority = nil
128 write_attribute(:priority_id, pid)
128 write_attribute(:priority_id, pid)
129 end
129 end
130
130
131 def tracker_id=(tid)
131 def tracker_id=(tid)
132 self.tracker = nil
132 self.tracker = nil
133 write_attribute(:tracker_id, tid)
133 write_attribute(:tracker_id, tid)
134 end
134 end
135
135
136 def estimated_hours=(h)
136 def estimated_hours=(h)
137 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
137 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
138 end
138 end
139
139
140 def validate
140 def validate
141 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
141 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
142 errors.add :due_date, :not_a_date
142 errors.add :due_date, :not_a_date
143 end
143 end
144
144
145 if self.due_date and self.start_date and self.due_date < self.start_date
145 if self.due_date and self.start_date and self.due_date < self.start_date
146 errors.add :due_date, :greater_than_start_date
146 errors.add :due_date, :greater_than_start_date
147 end
147 end
148
148
149 if start_date && soonest_start && start_date < soonest_start
149 if start_date && soonest_start && start_date < soonest_start
150 errors.add :start_date, :invalid
150 errors.add :start_date, :invalid
151 end
151 end
152
152
153 if fixed_version
153 if fixed_version
154 if !assignable_versions.include?(fixed_version)
154 if !assignable_versions.include?(fixed_version)
155 errors.add :fixed_version_id, :inclusion
155 errors.add :fixed_version_id, :inclusion
156 elsif reopened? && fixed_version.closed?
156 elsif reopened? && fixed_version.closed?
157 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
157 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
158 end
158 end
159 end
159 end
160
160
161 # Checks that the issue can not be added/moved to a disabled tracker
161 # Checks that the issue can not be added/moved to a disabled tracker
162 if project && (tracker_id_changed? || project_id_changed?)
162 if project && (tracker_id_changed? || project_id_changed?)
163 unless project.trackers.include?(tracker)
163 unless project.trackers.include?(tracker)
164 errors.add :tracker_id, :inclusion
164 errors.add :tracker_id, :inclusion
165 end
165 end
166 end
166 end
167 end
167 end
168
168
169 def before_create
169 def before_create
170 # default assignment based on category
170 # default assignment based on category
171 if assigned_to.nil? && category && category.assigned_to
171 if assigned_to.nil? && category && category.assigned_to
172 self.assigned_to = category.assigned_to
172 self.assigned_to = category.assigned_to
173 end
173 end
174 end
174 end
175
175
176 def after_save
176 def after_save
177 # Reload is needed in order to get the right status
177 # Reload is needed in order to get the right status
178 reload
178 reload
179
179
180 # Update start/due dates of following issues
180 # Update start/due dates of following issues
181 relations_from.each(&:set_issue_to_dates)
181 relations_from.each(&:set_issue_to_dates)
182
182
183 # Close duplicates if the issue was closed
183 # Close duplicates if the issue was closed
184 if @issue_before_change && !@issue_before_change.closed? && self.closed?
184 if @issue_before_change && !@issue_before_change.closed? && self.closed?
185 duplicates.each do |duplicate|
185 duplicates.each do |duplicate|
186 # Reload is need in case the duplicate was updated by a previous duplicate
186 # Reload is need in case the duplicate was updated by a previous duplicate
187 duplicate.reload
187 duplicate.reload
188 # Don't re-close it if it's already closed
188 # Don't re-close it if it's already closed
189 next if duplicate.closed?
189 next if duplicate.closed?
190 # Same user and notes
190 # Same user and notes
191 duplicate.init_journal(@current_journal.user, @current_journal.notes)
191 duplicate.init_journal(@current_journal.user, @current_journal.notes)
192 duplicate.update_attribute :status, self.status
192 duplicate.update_attribute :status, self.status
193 end
193 end
194 end
194 end
195 end
195 end
196
196
197 def init_journal(user, notes = "")
197 def init_journal(user, notes = "")
198 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
198 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
199 @issue_before_change = self.clone
199 @issue_before_change = self.clone
200 @issue_before_change.status = self.status
200 @issue_before_change.status = self.status
201 @custom_values_before_change = {}
201 @custom_values_before_change = {}
202 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
202 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
203 # Make sure updated_on is updated when adding a note.
203 # Make sure updated_on is updated when adding a note.
204 updated_on_will_change!
204 updated_on_will_change!
205 @current_journal
205 @current_journal
206 end
206 end
207
207
208 # Return true if the issue is closed, otherwise false
208 # Return true if the issue is closed, otherwise false
209 def closed?
209 def closed?
210 self.status.is_closed?
210 self.status.is_closed?
211 end
211 end
212
212
213 # Return true if the issue is being reopened
213 # Return true if the issue is being reopened
214 def reopened?
214 def reopened?
215 if !new_record? && status_id_changed?
215 if !new_record? && status_id_changed?
216 status_was = IssueStatus.find_by_id(status_id_was)
216 status_was = IssueStatus.find_by_id(status_id_was)
217 status_new = IssueStatus.find_by_id(status_id)
217 status_new = IssueStatus.find_by_id(status_id)
218 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
218 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
219 return true
219 return true
220 end
220 end
221 end
221 end
222 false
222 false
223 end
223 end
224
224
225 # Returns true if the issue is overdue
225 # Returns true if the issue is overdue
226 def overdue?
226 def overdue?
227 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
227 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
228 end
228 end
229
229
230 # Users the issue can be assigned to
230 # Users the issue can be assigned to
231 def assignable_users
231 def assignable_users
232 project.assignable_users
232 project.assignable_users
233 end
233 end
234
234
235 # Versions that the issue can be assigned to
235 # Versions that the issue can be assigned to
236 def assignable_versions
236 def assignable_versions
237 @assignable_versions ||= (project.versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
237 @assignable_versions ||= (project.versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
238 end
238 end
239
239
240 # Returns true if this issue is blocked by another issue that is still open
240 # Returns true if this issue is blocked by another issue that is still open
241 def blocked?
241 def blocked?
242 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
242 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
243 end
243 end
244
244
245 # Returns an array of status that user is able to apply
245 # Returns an array of status that user is able to apply
246 def new_statuses_allowed_to(user)
246 def new_statuses_allowed_to(user)
247 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
247 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
248 statuses << status unless statuses.empty?
248 statuses << status unless statuses.empty?
249 statuses = statuses.uniq.sort
249 statuses = statuses.uniq.sort
250 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
250 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
251 end
251 end
252
252
253 # Returns the mail adresses of users that should be notified for the issue
253 # Returns the mail adresses of users that should be notified
254 def recipients
254 def recipients
255 recipients = project.recipients
255 notified = project.notified_users
256 # Author and assignee are always notified unless they have been locked
256 # Author and assignee are always notified unless they have been locked
257 recipients << author.mail if author && author.active?
257 notified << author if author && author.active?
258 recipients << assigned_to.mail if assigned_to && assigned_to.active?
258 notified << assigned_to if assigned_to && assigned_to.active?
259 recipients.compact.uniq
259 notified.uniq!
260 # Remove users that can not view the issue
261 notified.reject! {|user| !visible?(user)}
262 notified.collect(&:mail)
263 end
264
265 # Returns the mail adresses of watchers that should be notified
266 def watcher_recipients
267 notified = watcher_users
268 notified.reject! {|user| !user.active? || !visible?(user)}
269 notified.collect(&:mail)
260 end
270 end
261
271
262 # Returns the total number of hours spent on this issue.
272 # Returns the total number of hours spent on this issue.
263 #
273 #
264 # Example:
274 # Example:
265 # spent_hours => 0
275 # spent_hours => 0
266 # spent_hours => 50
276 # spent_hours => 50
267 def spent_hours
277 def spent_hours
268 @spent_hours ||= time_entries.sum(:hours) || 0
278 @spent_hours ||= time_entries.sum(:hours) || 0
269 end
279 end
270
280
271 def relations
281 def relations
272 (relations_from + relations_to).sort
282 (relations_from + relations_to).sort
273 end
283 end
274
284
275 def all_dependent_issues
285 def all_dependent_issues
276 dependencies = []
286 dependencies = []
277 relations_from.each do |relation|
287 relations_from.each do |relation|
278 dependencies << relation.issue_to
288 dependencies << relation.issue_to
279 dependencies += relation.issue_to.all_dependent_issues
289 dependencies += relation.issue_to.all_dependent_issues
280 end
290 end
281 dependencies
291 dependencies
282 end
292 end
283
293
284 # Returns an array of issues that duplicate this one
294 # Returns an array of issues that duplicate this one
285 def duplicates
295 def duplicates
286 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
296 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
287 end
297 end
288
298
289 # Returns the due date or the target due date if any
299 # Returns the due date or the target due date if any
290 # Used on gantt chart
300 # Used on gantt chart
291 def due_before
301 def due_before
292 due_date || (fixed_version ? fixed_version.effective_date : nil)
302 due_date || (fixed_version ? fixed_version.effective_date : nil)
293 end
303 end
294
304
295 # Returns the time scheduled for this issue.
305 # Returns the time scheduled for this issue.
296 #
306 #
297 # Example:
307 # Example:
298 # Start Date: 2/26/09, End Date: 3/04/09
308 # Start Date: 2/26/09, End Date: 3/04/09
299 # duration => 6
309 # duration => 6
300 def duration
310 def duration
301 (start_date && due_date) ? due_date - start_date : 0
311 (start_date && due_date) ? due_date - start_date : 0
302 end
312 end
303
313
304 def soonest_start
314 def soonest_start
305 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
315 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
306 end
316 end
307
317
308 def to_s
318 def to_s
309 "#{tracker} ##{id}: #{subject}"
319 "#{tracker} ##{id}: #{subject}"
310 end
320 end
311
321
312 # Returns a string of css classes that apply to the issue
322 # Returns a string of css classes that apply to the issue
313 def css_classes
323 def css_classes
314 s = "issue status-#{status.position} priority-#{priority.position}"
324 s = "issue status-#{status.position} priority-#{priority.position}"
315 s << ' closed' if closed?
325 s << ' closed' if closed?
316 s << ' overdue' if overdue?
326 s << ' overdue' if overdue?
317 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
327 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
318 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
328 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
319 s
329 s
320 end
330 end
321
331
322 private
332 private
323
333
324 # Callback on attachment deletion
334 # Callback on attachment deletion
325 def attachment_removed(obj)
335 def attachment_removed(obj)
326 journal = init_journal(User.current)
336 journal = init_journal(User.current)
327 journal.details << JournalDetail.new(:property => 'attachment',
337 journal.details << JournalDetail.new(:property => 'attachment',
328 :prop_key => obj.id,
338 :prop_key => obj.id,
329 :old_value => obj.filename)
339 :old_value => obj.filename)
330 journal.save
340 journal.save
331 end
341 end
332
342
333 # Saves the changes in a Journal
343 # Saves the changes in a Journal
334 # Called after_save
344 # Called after_save
335 def create_journal
345 def create_journal
336 if @current_journal
346 if @current_journal
337 # attributes changes
347 # attributes changes
338 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
348 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
339 @current_journal.details << JournalDetail.new(:property => 'attr',
349 @current_journal.details << JournalDetail.new(:property => 'attr',
340 :prop_key => c,
350 :prop_key => c,
341 :old_value => @issue_before_change.send(c),
351 :old_value => @issue_before_change.send(c),
342 :value => send(c)) unless send(c)==@issue_before_change.send(c)
352 :value => send(c)) unless send(c)==@issue_before_change.send(c)
343 }
353 }
344 # custom fields changes
354 # custom fields changes
345 custom_values.each {|c|
355 custom_values.each {|c|
346 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
356 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
347 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
357 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
348 @current_journal.details << JournalDetail.new(:property => 'cf',
358 @current_journal.details << JournalDetail.new(:property => 'cf',
349 :prop_key => c.custom_field_id,
359 :prop_key => c.custom_field_id,
350 :old_value => @custom_values_before_change[c.custom_field_id],
360 :old_value => @custom_values_before_change[c.custom_field_id],
351 :value => c.value)
361 :value => c.value)
352 }
362 }
353 @current_journal.save
363 @current_journal.save
354 end
364 end
355 end
365 end
356 end
366 end
@@ -1,603 +1,608
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Specific overidden Activities
23 # Specific overidden Activities
24 has_many :time_entry_activities
24 has_many :time_entry_activities
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 has_many :member_principals, :class_name => 'Member',
26 has_many :member_principals, :class_name => 'Member',
27 :include => :principal,
27 :include => :principal,
28 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
28 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
29 has_many :users, :through => :members
29 has_many :users, :through => :members
30 has_many :principals, :through => :member_principals, :source => :principal
30 has_many :principals, :through => :member_principals, :source => :principal
31
31
32 has_many :enabled_modules, :dependent => :delete_all
32 has_many :enabled_modules, :dependent => :delete_all
33 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
33 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
34 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
34 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
35 has_many :issue_changes, :through => :issues, :source => :journals
35 has_many :issue_changes, :through => :issues, :source => :journals
36 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
36 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
37 has_many :time_entries, :dependent => :delete_all
37 has_many :time_entries, :dependent => :delete_all
38 has_many :queries, :dependent => :delete_all
38 has_many :queries, :dependent => :delete_all
39 has_many :documents, :dependent => :destroy
39 has_many :documents, :dependent => :destroy
40 has_many :news, :dependent => :delete_all, :include => :author
40 has_many :news, :dependent => :delete_all, :include => :author
41 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
41 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
42 has_many :boards, :dependent => :destroy, :order => "position ASC"
42 has_many :boards, :dependent => :destroy, :order => "position ASC"
43 has_one :repository, :dependent => :destroy
43 has_one :repository, :dependent => :destroy
44 has_many :changesets, :through => :repository
44 has_many :changesets, :through => :repository
45 has_one :wiki, :dependent => :destroy
45 has_one :wiki, :dependent => :destroy
46 # Custom field for the project issues
46 # Custom field for the project issues
47 has_and_belongs_to_many :issue_custom_fields,
47 has_and_belongs_to_many :issue_custom_fields,
48 :class_name => 'IssueCustomField',
48 :class_name => 'IssueCustomField',
49 :order => "#{CustomField.table_name}.position",
49 :order => "#{CustomField.table_name}.position",
50 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
50 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
51 :association_foreign_key => 'custom_field_id'
51 :association_foreign_key => 'custom_field_id'
52
52
53 acts_as_nested_set :order => 'name', :dependent => :destroy
53 acts_as_nested_set :order => 'name', :dependent => :destroy
54 acts_as_attachable :view_permission => :view_files,
54 acts_as_attachable :view_permission => :view_files,
55 :delete_permission => :manage_files
55 :delete_permission => :manage_files
56
56
57 acts_as_customizable
57 acts_as_customizable
58 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
58 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
59 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
59 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
60 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
60 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
61 :author => nil
61 :author => nil
62
62
63 attr_protected :status, :enabled_module_names
63 attr_protected :status, :enabled_module_names
64
64
65 validates_presence_of :name, :identifier
65 validates_presence_of :name, :identifier
66 validates_uniqueness_of :name, :identifier
66 validates_uniqueness_of :name, :identifier
67 validates_associated :repository, :wiki
67 validates_associated :repository, :wiki
68 validates_length_of :name, :maximum => 30
68 validates_length_of :name, :maximum => 30
69 validates_length_of :homepage, :maximum => 255
69 validates_length_of :homepage, :maximum => 255
70 validates_length_of :identifier, :in => 1..20
70 validates_length_of :identifier, :in => 1..20
71 # donwcase letters, digits, dashes but not digits only
71 # donwcase letters, digits, dashes but not digits only
72 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
72 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
73 # reserved words
73 # reserved words
74 validates_exclusion_of :identifier, :in => %w( new )
74 validates_exclusion_of :identifier, :in => %w( new )
75
75
76 before_destroy :delete_all_members
76 before_destroy :delete_all_members
77
77
78 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
78 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
79 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
79 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
80 named_scope :all_public, { :conditions => { :is_public => true } }
80 named_scope :all_public, { :conditions => { :is_public => true } }
81 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
81 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
82
82
83 def identifier=(identifier)
83 def identifier=(identifier)
84 super unless identifier_frozen?
84 super unless identifier_frozen?
85 end
85 end
86
86
87 def identifier_frozen?
87 def identifier_frozen?
88 errors[:identifier].nil? && !(new_record? || identifier.blank?)
88 errors[:identifier].nil? && !(new_record? || identifier.blank?)
89 end
89 end
90
90
91 # returns latest created projects
91 # returns latest created projects
92 # non public projects will be returned only if user is a member of those
92 # non public projects will be returned only if user is a member of those
93 def self.latest(user=nil, count=5)
93 def self.latest(user=nil, count=5)
94 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
94 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 end
95 end
96
96
97 # Returns a SQL :conditions string used to find all active projects for the specified user.
97 # Returns a SQL :conditions string used to find all active projects for the specified user.
98 #
98 #
99 # Examples:
99 # Examples:
100 # Projects.visible_by(admin) => "projects.status = 1"
100 # Projects.visible_by(admin) => "projects.status = 1"
101 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
101 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
102 def self.visible_by(user=nil)
102 def self.visible_by(user=nil)
103 user ||= User.current
103 user ||= User.current
104 if user && user.admin?
104 if user && user.admin?
105 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
105 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 elsif user && user.memberships.any?
106 elsif user && user.memberships.any?
107 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
107 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
108 else
108 else
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
109 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 end
110 end
111 end
111 end
112
112
113 def self.allowed_to_condition(user, permission, options={})
113 def self.allowed_to_condition(user, permission, options={})
114 statements = []
114 statements = []
115 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
115 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 if perm = Redmine::AccessControl.permission(permission)
116 if perm = Redmine::AccessControl.permission(permission)
117 unless perm.project_module.nil?
117 unless perm.project_module.nil?
118 # If the permission belongs to a project module, make sure the module is enabled
118 # If the permission belongs to a project module, make sure the module is enabled
119 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
119 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
120 end
120 end
121 end
121 end
122 if options[:project]
122 if options[:project]
123 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
123 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
124 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 base_statement = "(#{project_statement}) AND (#{base_statement})"
125 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 end
126 end
127 if user.admin?
127 if user.admin?
128 # no restriction
128 # no restriction
129 else
129 else
130 statements << "1=0"
130 statements << "1=0"
131 if user.logged?
131 if user.logged?
132 if Role.non_member.allowed_to?(permission) && !options[:member]
132 if Role.non_member.allowed_to?(permission) && !options[:member]
133 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
133 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
134 end
134 end
135 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
135 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
136 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
136 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
137 else
137 else
138 if Role.anonymous.allowed_to?(permission) && !options[:member]
138 if Role.anonymous.allowed_to?(permission) && !options[:member]
139 # anonymous user allowed on public project
139 # anonymous user allowed on public project
140 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
140 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
141 end
141 end
142 end
142 end
143 end
143 end
144 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
144 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
145 end
145 end
146
146
147 # Returns the Systemwide and project specific activities
147 # Returns the Systemwide and project specific activities
148 def activities(include_inactive=false)
148 def activities(include_inactive=false)
149 if include_inactive
149 if include_inactive
150 return all_activities
150 return all_activities
151 else
151 else
152 return active_activities
152 return active_activities
153 end
153 end
154 end
154 end
155
155
156 # Will create a new Project specific Activity or update an existing one
156 # Will create a new Project specific Activity or update an existing one
157 #
157 #
158 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
158 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
159 # does not successfully save.
159 # does not successfully save.
160 def update_or_create_time_entry_activity(id, activity_hash)
160 def update_or_create_time_entry_activity(id, activity_hash)
161 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
161 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
162 self.create_time_entry_activity_if_needed(activity_hash)
162 self.create_time_entry_activity_if_needed(activity_hash)
163 else
163 else
164 activity = project.time_entry_activities.find_by_id(id.to_i)
164 activity = project.time_entry_activities.find_by_id(id.to_i)
165 activity.update_attributes(activity_hash) if activity
165 activity.update_attributes(activity_hash) if activity
166 end
166 end
167 end
167 end
168
168
169 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
169 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
170 #
170 #
171 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
171 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
172 # does not successfully save.
172 # does not successfully save.
173 def create_time_entry_activity_if_needed(activity)
173 def create_time_entry_activity_if_needed(activity)
174 if activity['parent_id']
174 if activity['parent_id']
175
175
176 parent_activity = TimeEntryActivity.find(activity['parent_id'])
176 parent_activity = TimeEntryActivity.find(activity['parent_id'])
177 activity['name'] = parent_activity.name
177 activity['name'] = parent_activity.name
178 activity['position'] = parent_activity.position
178 activity['position'] = parent_activity.position
179
179
180 if Enumeration.overridding_change?(activity, parent_activity)
180 if Enumeration.overridding_change?(activity, parent_activity)
181 project_activity = self.time_entry_activities.create(activity)
181 project_activity = self.time_entry_activities.create(activity)
182
182
183 if project_activity.new_record?
183 if project_activity.new_record?
184 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
184 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
185 else
185 else
186 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
186 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
187 end
187 end
188 end
188 end
189 end
189 end
190 end
190 end
191
191
192 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
192 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
193 #
193 #
194 # Examples:
194 # Examples:
195 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
195 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
196 # project.project_condition(false) => "projects.id = 1"
196 # project.project_condition(false) => "projects.id = 1"
197 def project_condition(with_subprojects)
197 def project_condition(with_subprojects)
198 cond = "#{Project.table_name}.id = #{id}"
198 cond = "#{Project.table_name}.id = #{id}"
199 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
199 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
200 cond
200 cond
201 end
201 end
202
202
203 def self.find(*args)
203 def self.find(*args)
204 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
204 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
205 project = find_by_identifier(*args)
205 project = find_by_identifier(*args)
206 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
206 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
207 project
207 project
208 else
208 else
209 super
209 super
210 end
210 end
211 end
211 end
212
212
213 def to_param
213 def to_param
214 # id is used for projects with a numeric identifier (compatibility)
214 # id is used for projects with a numeric identifier (compatibility)
215 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
215 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
216 end
216 end
217
217
218 def active?
218 def active?
219 self.status == STATUS_ACTIVE
219 self.status == STATUS_ACTIVE
220 end
220 end
221
221
222 # Archives the project and its descendants recursively
222 # Archives the project and its descendants recursively
223 def archive
223 def archive
224 # Archive subprojects if any
224 # Archive subprojects if any
225 children.each do |subproject|
225 children.each do |subproject|
226 subproject.archive
226 subproject.archive
227 end
227 end
228 update_attribute :status, STATUS_ARCHIVED
228 update_attribute :status, STATUS_ARCHIVED
229 end
229 end
230
230
231 # Unarchives the project
231 # Unarchives the project
232 # All its ancestors must be active
232 # All its ancestors must be active
233 def unarchive
233 def unarchive
234 return false if ancestors.detect {|a| !a.active?}
234 return false if ancestors.detect {|a| !a.active?}
235 update_attribute :status, STATUS_ACTIVE
235 update_attribute :status, STATUS_ACTIVE
236 end
236 end
237
237
238 # Returns an array of projects the project can be moved to
238 # Returns an array of projects the project can be moved to
239 # by the current user
239 # by the current user
240 def allowed_parents
240 def allowed_parents
241 return @allowed_parents if @allowed_parents
241 return @allowed_parents if @allowed_parents
242 @allowed_parents = (Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_project, :member => true)) - self_and_descendants)
242 @allowed_parents = (Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_project, :member => true)) - self_and_descendants)
243 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
243 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
244 @allowed_parents << parent
244 @allowed_parents << parent
245 end
245 end
246 @allowed_parents
246 @allowed_parents
247 end
247 end
248
248
249 # Sets the parent of the project with authorization check
249 # Sets the parent of the project with authorization check
250 def set_allowed_parent!(p)
250 def set_allowed_parent!(p)
251 unless p.nil? || p.is_a?(Project)
251 unless p.nil? || p.is_a?(Project)
252 if p.to_s.blank?
252 if p.to_s.blank?
253 p = nil
253 p = nil
254 else
254 else
255 p = Project.find_by_id(p)
255 p = Project.find_by_id(p)
256 return false unless p
256 return false unless p
257 end
257 end
258 end
258 end
259 if p.nil?
259 if p.nil?
260 if !new_record? && allowed_parents.empty?
260 if !new_record? && allowed_parents.empty?
261 return false
261 return false
262 end
262 end
263 elsif !allowed_parents.include?(p)
263 elsif !allowed_parents.include?(p)
264 return false
264 return false
265 end
265 end
266 set_parent!(p)
266 set_parent!(p)
267 end
267 end
268
268
269 # Sets the parent of the project
269 # Sets the parent of the project
270 # Argument can be either a Project, a String, a Fixnum or nil
270 # Argument can be either a Project, a String, a Fixnum or nil
271 def set_parent!(p)
271 def set_parent!(p)
272 unless p.nil? || p.is_a?(Project)
272 unless p.nil? || p.is_a?(Project)
273 if p.to_s.blank?
273 if p.to_s.blank?
274 p = nil
274 p = nil
275 else
275 else
276 p = Project.find_by_id(p)
276 p = Project.find_by_id(p)
277 return false unless p
277 return false unless p
278 end
278 end
279 end
279 end
280 if p == parent && !p.nil?
280 if p == parent && !p.nil?
281 # Nothing to do
281 # Nothing to do
282 true
282 true
283 elsif p.nil? || (p.active? && move_possible?(p))
283 elsif p.nil? || (p.active? && move_possible?(p))
284 # Insert the project so that target's children or root projects stay alphabetically sorted
284 # Insert the project so that target's children or root projects stay alphabetically sorted
285 sibs = (p.nil? ? self.class.roots : p.children)
285 sibs = (p.nil? ? self.class.roots : p.children)
286 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
286 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
287 if to_be_inserted_before
287 if to_be_inserted_before
288 move_to_left_of(to_be_inserted_before)
288 move_to_left_of(to_be_inserted_before)
289 elsif p.nil?
289 elsif p.nil?
290 if sibs.empty?
290 if sibs.empty?
291 # move_to_root adds the project in first (ie. left) position
291 # move_to_root adds the project in first (ie. left) position
292 move_to_root
292 move_to_root
293 else
293 else
294 move_to_right_of(sibs.last) unless self == sibs.last
294 move_to_right_of(sibs.last) unless self == sibs.last
295 end
295 end
296 else
296 else
297 # move_to_child_of adds the project in last (ie.right) position
297 # move_to_child_of adds the project in last (ie.right) position
298 move_to_child_of(p)
298 move_to_child_of(p)
299 end
299 end
300 true
300 true
301 else
301 else
302 # Can not move to the given target
302 # Can not move to the given target
303 false
303 false
304 end
304 end
305 end
305 end
306
306
307 # Returns an array of the trackers used by the project and its active sub projects
307 # Returns an array of the trackers used by the project and its active sub projects
308 def rolled_up_trackers
308 def rolled_up_trackers
309 @rolled_up_trackers ||=
309 @rolled_up_trackers ||=
310 Tracker.find(:all, :include => :projects,
310 Tracker.find(:all, :include => :projects,
311 :select => "DISTINCT #{Tracker.table_name}.*",
311 :select => "DISTINCT #{Tracker.table_name}.*",
312 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
312 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
313 :order => "#{Tracker.table_name}.position")
313 :order => "#{Tracker.table_name}.position")
314 end
314 end
315
315
316 # Closes open and locked project versions that are completed
316 # Closes open and locked project versions that are completed
317 def close_completed_versions
317 def close_completed_versions
318 Version.transaction do
318 Version.transaction do
319 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
319 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
320 if version.completed?
320 if version.completed?
321 version.update_attribute(:status, 'closed')
321 version.update_attribute(:status, 'closed')
322 end
322 end
323 end
323 end
324 end
324 end
325 end
325 end
326
326
327 # Returns a hash of project users grouped by role
327 # Returns a hash of project users grouped by role
328 def users_by_role
328 def users_by_role
329 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
329 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
330 m.roles.each do |r|
330 m.roles.each do |r|
331 h[r] ||= []
331 h[r] ||= []
332 h[r] << m.user
332 h[r] << m.user
333 end
333 end
334 h
334 h
335 end
335 end
336 end
336 end
337
337
338 # Deletes all project's members
338 # Deletes all project's members
339 def delete_all_members
339 def delete_all_members
340 me, mr = Member.table_name, MemberRole.table_name
340 me, mr = Member.table_name, MemberRole.table_name
341 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
341 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
342 Member.delete_all(['project_id = ?', id])
342 Member.delete_all(['project_id = ?', id])
343 end
343 end
344
344
345 # Users issues can be assigned to
345 # Users issues can be assigned to
346 def assignable_users
346 def assignable_users
347 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
347 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
348 end
348 end
349
349
350 # Returns the mail adresses of users that should be always notified on project events
350 # Returns the mail adresses of users that should be always notified on project events
351 def recipients
351 def recipients
352 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
352 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
353 end
353 end
354
354
355 # Returns the users that should be notified on project events
356 def notified_users
357 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
358 end
359
355 # Returns an array of all custom fields enabled for project issues
360 # Returns an array of all custom fields enabled for project issues
356 # (explictly associated custom fields and custom fields enabled for all projects)
361 # (explictly associated custom fields and custom fields enabled for all projects)
357 def all_issue_custom_fields
362 def all_issue_custom_fields
358 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
363 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
359 end
364 end
360
365
361 def project
366 def project
362 self
367 self
363 end
368 end
364
369
365 def <=>(project)
370 def <=>(project)
366 name.downcase <=> project.name.downcase
371 name.downcase <=> project.name.downcase
367 end
372 end
368
373
369 def to_s
374 def to_s
370 name
375 name
371 end
376 end
372
377
373 # Returns a short description of the projects (first lines)
378 # Returns a short description of the projects (first lines)
374 def short_description(length = 255)
379 def short_description(length = 255)
375 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
380 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
376 end
381 end
377
382
378 # Return true if this project is allowed to do the specified action.
383 # Return true if this project is allowed to do the specified action.
379 # action can be:
384 # action can be:
380 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
385 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
381 # * a permission Symbol (eg. :edit_project)
386 # * a permission Symbol (eg. :edit_project)
382 def allows_to?(action)
387 def allows_to?(action)
383 if action.is_a? Hash
388 if action.is_a? Hash
384 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
389 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
385 else
390 else
386 allowed_permissions.include? action
391 allowed_permissions.include? action
387 end
392 end
388 end
393 end
389
394
390 def module_enabled?(module_name)
395 def module_enabled?(module_name)
391 module_name = module_name.to_s
396 module_name = module_name.to_s
392 enabled_modules.detect {|m| m.name == module_name}
397 enabled_modules.detect {|m| m.name == module_name}
393 end
398 end
394
399
395 def enabled_module_names=(module_names)
400 def enabled_module_names=(module_names)
396 if module_names && module_names.is_a?(Array)
401 if module_names && module_names.is_a?(Array)
397 module_names = module_names.collect(&:to_s)
402 module_names = module_names.collect(&:to_s)
398 # remove disabled modules
403 # remove disabled modules
399 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
404 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
400 # add new modules
405 # add new modules
401 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
406 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
402 else
407 else
403 enabled_modules.clear
408 enabled_modules.clear
404 end
409 end
405 end
410 end
406
411
407 # Returns an auto-generated project identifier based on the last identifier used
412 # Returns an auto-generated project identifier based on the last identifier used
408 def self.next_identifier
413 def self.next_identifier
409 p = Project.find(:first, :order => 'created_on DESC')
414 p = Project.find(:first, :order => 'created_on DESC')
410 p.nil? ? nil : p.identifier.to_s.succ
415 p.nil? ? nil : p.identifier.to_s.succ
411 end
416 end
412
417
413 # Copies and saves the Project instance based on the +project+.
418 # Copies and saves the Project instance based on the +project+.
414 # Duplicates the source project's:
419 # Duplicates the source project's:
415 # * Wiki
420 # * Wiki
416 # * Versions
421 # * Versions
417 # * Categories
422 # * Categories
418 # * Issues
423 # * Issues
419 # * Members
424 # * Members
420 # * Queries
425 # * Queries
421 #
426 #
422 # Accepts an +options+ argument to specify what to copy
427 # Accepts an +options+ argument to specify what to copy
423 #
428 #
424 # Examples:
429 # Examples:
425 # project.copy(1) # => copies everything
430 # project.copy(1) # => copies everything
426 # project.copy(1, :only => 'members') # => copies members only
431 # project.copy(1, :only => 'members') # => copies members only
427 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
432 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
428 def copy(project, options={})
433 def copy(project, options={})
429 project = project.is_a?(Project) ? project : Project.find(project)
434 project = project.is_a?(Project) ? project : Project.find(project)
430
435
431 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
436 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
432 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
437 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
433
438
434 Project.transaction do
439 Project.transaction do
435 if save
440 if save
436 reload
441 reload
437 to_be_copied.each do |name|
442 to_be_copied.each do |name|
438 send "copy_#{name}", project
443 send "copy_#{name}", project
439 end
444 end
440 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
445 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
441 save
446 save
442 end
447 end
443 end
448 end
444 end
449 end
445
450
446
451
447 # Copies +project+ and returns the new instance. This will not save
452 # Copies +project+ and returns the new instance. This will not save
448 # the copy
453 # the copy
449 def self.copy_from(project)
454 def self.copy_from(project)
450 begin
455 begin
451 project = project.is_a?(Project) ? project : Project.find(project)
456 project = project.is_a?(Project) ? project : Project.find(project)
452 if project
457 if project
453 # clear unique attributes
458 # clear unique attributes
454 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
459 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
455 copy = Project.new(attributes)
460 copy = Project.new(attributes)
456 copy.enabled_modules = project.enabled_modules
461 copy.enabled_modules = project.enabled_modules
457 copy.trackers = project.trackers
462 copy.trackers = project.trackers
458 copy.custom_values = project.custom_values.collect {|v| v.clone}
463 copy.custom_values = project.custom_values.collect {|v| v.clone}
459 copy.issue_custom_fields = project.issue_custom_fields
464 copy.issue_custom_fields = project.issue_custom_fields
460 return copy
465 return copy
461 else
466 else
462 return nil
467 return nil
463 end
468 end
464 rescue ActiveRecord::RecordNotFound
469 rescue ActiveRecord::RecordNotFound
465 return nil
470 return nil
466 end
471 end
467 end
472 end
468
473
469 private
474 private
470
475
471 # Copies wiki from +project+
476 # Copies wiki from +project+
472 def copy_wiki(project)
477 def copy_wiki(project)
473 # Check that the source project has a wiki first
478 # Check that the source project has a wiki first
474 unless project.wiki.nil?
479 unless project.wiki.nil?
475 self.wiki ||= Wiki.new
480 self.wiki ||= Wiki.new
476 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
481 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
477 project.wiki.pages.each do |page|
482 project.wiki.pages.each do |page|
478 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
483 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
479 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
484 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
480 new_wiki_page.content = new_wiki_content
485 new_wiki_page.content = new_wiki_content
481 wiki.pages << new_wiki_page
486 wiki.pages << new_wiki_page
482 end
487 end
483 end
488 end
484 end
489 end
485
490
486 # Copies versions from +project+
491 # Copies versions from +project+
487 def copy_versions(project)
492 def copy_versions(project)
488 project.versions.each do |version|
493 project.versions.each do |version|
489 new_version = Version.new
494 new_version = Version.new
490 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
495 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
491 self.versions << new_version
496 self.versions << new_version
492 end
497 end
493 end
498 end
494
499
495 # Copies issue categories from +project+
500 # Copies issue categories from +project+
496 def copy_issue_categories(project)
501 def copy_issue_categories(project)
497 project.issue_categories.each do |issue_category|
502 project.issue_categories.each do |issue_category|
498 new_issue_category = IssueCategory.new
503 new_issue_category = IssueCategory.new
499 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
504 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
500 self.issue_categories << new_issue_category
505 self.issue_categories << new_issue_category
501 end
506 end
502 end
507 end
503
508
504 # Copies issues from +project+
509 # Copies issues from +project+
505 def copy_issues(project)
510 def copy_issues(project)
506 project.issues.each do |issue|
511 project.issues.each do |issue|
507 new_issue = Issue.new
512 new_issue = Issue.new
508 new_issue.copy_from(issue)
513 new_issue.copy_from(issue)
509 # Reassign fixed_versions by name, since names are unique per
514 # Reassign fixed_versions by name, since names are unique per
510 # project and the versions for self are not yet saved
515 # project and the versions for self are not yet saved
511 if issue.fixed_version
516 if issue.fixed_version
512 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
517 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
513 end
518 end
514 # Reassign the category by name, since names are unique per
519 # Reassign the category by name, since names are unique per
515 # project and the categories for self are not yet saved
520 # project and the categories for self are not yet saved
516 if issue.category
521 if issue.category
517 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
522 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
518 end
523 end
519 self.issues << new_issue
524 self.issues << new_issue
520 end
525 end
521 end
526 end
522
527
523 # Copies members from +project+
528 # Copies members from +project+
524 def copy_members(project)
529 def copy_members(project)
525 project.members.each do |member|
530 project.members.each do |member|
526 new_member = Member.new
531 new_member = Member.new
527 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
532 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
528 new_member.role_ids = member.role_ids.dup
533 new_member.role_ids = member.role_ids.dup
529 new_member.project = self
534 new_member.project = self
530 self.members << new_member
535 self.members << new_member
531 end
536 end
532 end
537 end
533
538
534 # Copies queries from +project+
539 # Copies queries from +project+
535 def copy_queries(project)
540 def copy_queries(project)
536 project.queries.each do |query|
541 project.queries.each do |query|
537 new_query = Query.new
542 new_query = Query.new
538 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
543 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
539 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
544 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
540 new_query.project = self
545 new_query.project = self
541 self.queries << new_query
546 self.queries << new_query
542 end
547 end
543 end
548 end
544
549
545 # Copies boards from +project+
550 # Copies boards from +project+
546 def copy_boards(project)
551 def copy_boards(project)
547 project.boards.each do |board|
552 project.boards.each do |board|
548 new_board = Board.new
553 new_board = Board.new
549 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
554 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
550 new_board.project = self
555 new_board.project = self
551 self.boards << new_board
556 self.boards << new_board
552 end
557 end
553 end
558 end
554
559
555 def allowed_permissions
560 def allowed_permissions
556 @allowed_permissions ||= begin
561 @allowed_permissions ||= begin
557 module_names = enabled_modules.collect {|m| m.name}
562 module_names = enabled_modules.collect {|m| m.name}
558 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
563 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
559 end
564 end
560 end
565 end
561
566
562 def allowed_actions
567 def allowed_actions
563 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
568 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
564 end
569 end
565
570
566 # Returns all the active Systemwide and project specific activities
571 # Returns all the active Systemwide and project specific activities
567 def active_activities
572 def active_activities
568 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
573 overridden_activity_ids = self.time_entry_activities.active.collect(&:parent_id)
569
574
570 if overridden_activity_ids.empty?
575 if overridden_activity_ids.empty?
571 return TimeEntryActivity.shared.active
576 return TimeEntryActivity.shared.active
572 else
577 else
573 return system_activities_and_project_overrides
578 return system_activities_and_project_overrides
574 end
579 end
575 end
580 end
576
581
577 # Returns all the Systemwide and project specific activities
582 # Returns all the Systemwide and project specific activities
578 # (inactive and active)
583 # (inactive and active)
579 def all_activities
584 def all_activities
580 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
585 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
581
586
582 if overridden_activity_ids.empty?
587 if overridden_activity_ids.empty?
583 return TimeEntryActivity.shared
588 return TimeEntryActivity.shared
584 else
589 else
585 return system_activities_and_project_overrides(true)
590 return system_activities_and_project_overrides(true)
586 end
591 end
587 end
592 end
588
593
589 # Returns the systemwide active activities merged with the project specific overrides
594 # Returns the systemwide active activities merged with the project specific overrides
590 def system_activities_and_project_overrides(include_inactive=false)
595 def system_activities_and_project_overrides(include_inactive=false)
591 if include_inactive
596 if include_inactive
592 return TimeEntryActivity.shared.
597 return TimeEntryActivity.shared.
593 find(:all,
598 find(:all,
594 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
599 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
595 self.time_entry_activities
600 self.time_entry_activities
596 else
601 else
597 return TimeEntryActivity.shared.active.
602 return TimeEntryActivity.shared.active.
598 find(:all,
603 find(:all,
599 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
604 :conditions => ["id NOT IN (?)", self.time_entry_activities.active.collect(&:parent_id)]) +
600 self.time_entry_activities.active
605 self.time_entry_activities.active
601 end
606 end
602 end
607 end
603 end
608 end
@@ -1,191 +1,191
1 ---
1 ---
2 issues_001:
2 issues_001:
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
3 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
4 project_id: 1
4 project_id: 1
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
5 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
6 priority_id: 4
6 priority_id: 4
7 subject: Can't print recipes
7 subject: Can't print recipes
8 id: 1
8 id: 1
9 fixed_version_id:
9 fixed_version_id:
10 category_id: 1
10 category_id: 1
11 description: Unable to print recipes
11 description: Unable to print recipes
12 tracker_id: 1
12 tracker_id: 1
13 assigned_to_id:
13 assigned_to_id:
14 author_id: 2
14 author_id: 2
15 status_id: 1
15 status_id: 1
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
16 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
17 due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
18 issues_002:
18 issues_002:
19 created_on: 2006-07-19 21:04:21 +02:00
19 created_on: 2006-07-19 21:04:21 +02:00
20 project_id: 1
20 project_id: 1
21 updated_on: 2006-07-19 21:09:50 +02:00
21 updated_on: 2006-07-19 21:09:50 +02:00
22 priority_id: 5
22 priority_id: 5
23 subject: Add ingredients categories
23 subject: Add ingredients categories
24 id: 2
24 id: 2
25 fixed_version_id: 2
25 fixed_version_id: 2
26 category_id:
26 category_id:
27 description: Ingredients of the recipe should be classified by categories
27 description: Ingredients of the recipe should be classified by categories
28 tracker_id: 2
28 tracker_id: 2
29 assigned_to_id: 3
29 assigned_to_id: 3
30 author_id: 2
30 author_id: 2
31 status_id: 2
31 status_id: 2
32 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
32 start_date: <%= 2.day.ago.to_date.to_s(:db) %>
33 due_date:
33 due_date:
34 issues_003:
34 issues_003:
35 created_on: 2006-07-19 21:07:27 +02:00
35 created_on: 2006-07-19 21:07:27 +02:00
36 project_id: 1
36 project_id: 1
37 updated_on: 2006-07-19 21:07:27 +02:00
37 updated_on: 2006-07-19 21:07:27 +02:00
38 priority_id: 4
38 priority_id: 4
39 subject: Error 281 when updating a recipe
39 subject: Error 281 when updating a recipe
40 id: 3
40 id: 3
41 fixed_version_id:
41 fixed_version_id:
42 category_id:
42 category_id:
43 description: Error 281 is encountered when saving a recipe
43 description: Error 281 is encountered when saving a recipe
44 tracker_id: 1
44 tracker_id: 1
45 assigned_to_id: 3
45 assigned_to_id: 3
46 author_id: 2
46 author_id: 2
47 status_id: 1
47 status_id: 1
48 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
48 start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
49 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
49 due_date: <%= 40.day.ago.to_date.to_s(:db) %>
50 issues_004:
50 issues_004:
51 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
51 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
52 project_id: 2
52 project_id: 2
53 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
53 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
54 priority_id: 4
54 priority_id: 4
55 subject: Issue on project 2
55 subject: Issue on project 2
56 id: 4
56 id: 4
57 fixed_version_id:
57 fixed_version_id:
58 category_id:
58 category_id:
59 description: Issue on project 2
59 description: Issue on project 2
60 tracker_id: 1
60 tracker_id: 1
61 assigned_to_id: 2
61 assigned_to_id: 2
62 author_id: 2
62 author_id: 2
63 status_id: 1
63 status_id: 1
64 issues_005:
64 issues_005:
65 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
65 created_on: <%= 5.days.ago.to_date.to_s(:db) %>
66 project_id: 3
66 project_id: 3
67 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
67 updated_on: <%= 2.days.ago.to_date.to_s(:db) %>
68 priority_id: 4
68 priority_id: 4
69 subject: Subproject issue
69 subject: Subproject issue
70 id: 5
70 id: 5
71 fixed_version_id:
71 fixed_version_id:
72 category_id:
72 category_id:
73 description: This is an issue on a cookbook subproject
73 description: This is an issue on a cookbook subproject
74 tracker_id: 1
74 tracker_id: 1
75 assigned_to_id:
75 assigned_to_id:
76 author_id: 2
76 author_id: 2
77 status_id: 1
77 status_id: 1
78 issues_006:
78 issues_006:
79 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
79 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
80 project_id: 5
80 project_id: 5
81 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
81 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
82 priority_id: 4
82 priority_id: 4
83 subject: Issue of a private subproject
83 subject: Issue of a private subproject
84 id: 6
84 id: 6
85 fixed_version_id:
85 fixed_version_id:
86 category_id:
86 category_id:
87 description: This is an issue of a private subproject of cookbook
87 description: This is an issue of a private subproject of cookbook
88 tracker_id: 1
88 tracker_id: 1
89 assigned_to_id:
89 assigned_to_id:
90 author_id: 2
90 author_id: 2
91 status_id: 1
91 status_id: 1
92 start_date: <%= Date.today.to_s(:db) %>
92 start_date: <%= Date.today.to_s(:db) %>
93 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
93 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
94 issues_007:
94 issues_007:
95 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
95 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
96 project_id: 1
96 project_id: 1
97 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
97 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
98 priority_id: 5
98 priority_id: 5
99 subject: Issue due today
99 subject: Issue due today
100 id: 7
100 id: 7
101 fixed_version_id:
101 fixed_version_id:
102 category_id:
102 category_id:
103 description: This is an issue that is due today
103 description: This is an issue that is due today
104 tracker_id: 1
104 tracker_id: 1
105 assigned_to_id:
105 assigned_to_id:
106 author_id: 2
106 author_id: 2
107 status_id: 1
107 status_id: 1
108 start_date: <%= 10.days.ago.to_s(:db) %>
108 start_date: <%= 10.days.ago.to_s(:db) %>
109 due_date: <%= Date.today.to_s(:db) %>
109 due_date: <%= Date.today.to_s(:db) %>
110 lock_version: 0
110 lock_version: 0
111 issues_008:
111 issues_008:
112 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
112 created_on: <%= 10.days.ago.to_date.to_s(:db) %>
113 project_id: 1
113 project_id: 1
114 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
114 updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
115 priority_id: 5
115 priority_id: 5
116 subject: Closed issue
116 subject: Closed issue
117 id: 8
117 id: 8
118 fixed_version_id:
118 fixed_version_id:
119 category_id:
119 category_id:
120 description: This is a closed issue.
120 description: This is a closed issue.
121 tracker_id: 1
121 tracker_id: 1
122 assigned_to_id:
122 assigned_to_id:
123 author_id: 2
123 author_id: 2
124 status_id: 5
124 status_id: 5
125 start_date:
125 start_date:
126 due_date:
126 due_date:
127 lock_version: 0
127 lock_version: 0
128 issues_009:
128 issues_009:
129 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
129 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
130 project_id: 5
130 project_id: 5
131 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
131 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
132 priority_id: 5
132 priority_id: 5
133 subject: Blocked Issue
133 subject: Blocked Issue
134 id: 9
134 id: 9
135 fixed_version_id:
135 fixed_version_id:
136 category_id:
136 category_id:
137 description: This is an issue that is blocked by issue #10
137 description: This is an issue that is blocked by issue #10
138 tracker_id: 1
138 tracker_id: 1
139 assigned_to_id:
139 assigned_to_id:
140 author_id: 2
140 author_id: 2
141 status_id: 1
141 status_id: 1
142 start_date: <%= Date.today.to_s(:db) %>
142 start_date: <%= Date.today.to_s(:db) %>
143 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
143 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
144 issues_010:
144 issues_010:
145 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
145 created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
146 project_id: 5
146 project_id: 5
147 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
147 updated_on: <%= 1.minute.ago.to_date.to_s(:db) %>
148 priority_id: 5
148 priority_id: 5
149 subject: Issue Doing the Blocking
149 subject: Issue Doing the Blocking
150 id: 10
150 id: 10
151 fixed_version_id:
151 fixed_version_id:
152 category_id:
152 category_id:
153 description: This is an issue that blocks issue #9
153 description: This is an issue that blocks issue #9
154 tracker_id: 1
154 tracker_id: 1
155 assigned_to_id:
155 assigned_to_id:
156 author_id: 2
156 author_id: 2
157 status_id: 1
157 status_id: 1
158 start_date: <%= Date.today.to_s(:db) %>
158 start_date: <%= Date.today.to_s(:db) %>
159 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
159 due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
160 issues_011:
160 issues_011:
161 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
161 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
162 project_id: 1
162 project_id: 1
163 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
163 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
164 priority_id: 5
164 priority_id: 5
165 subject: Closed issue on a closed version
165 subject: Closed issue on a closed version
166 id: 11
166 id: 11
167 fixed_version_id: 1
167 fixed_version_id: 1
168 category_id: 1
168 category_id: 1
169 description:
169 description:
170 tracker_id: 1
170 tracker_id: 1
171 assigned_to_id:
171 assigned_to_id:
172 author_id: 2
172 author_id: 2
173 status_id: 5
173 status_id: 5
174 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
174 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
175 due_date:
175 due_date:
176 issues_012:
176 issues_012:
177 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
177 created_on: <%= 3.days.ago.to_date.to_s(:db) %>
178 project_id: 1
178 project_id: 1
179 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
179 updated_on: <%= 1.day.ago.to_date.to_s(:db) %>
180 priority_id: 5
180 priority_id: 5
181 subject: Closed issue on a locked version
181 subject: Closed issue on a locked version
182 id: 12
182 id: 12
183 fixed_version_id: 2
183 fixed_version_id: 2
184 category_id: 1
184 category_id: 1
185 description:
185 description:
186 tracker_id: 1
186 tracker_id: 1
187 assigned_to_id:
187 assigned_to_id:
188 author_id: 2
188 author_id: 3
189 status_id: 5
189 status_id: 5
190 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
190 start_date: <%= 1.day.ago.to_date.to_s(:db) %>
191 due_date:
191 due_date:
@@ -1,426 +1,443
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class IssueTest < ActiveSupport::TestCase
20 class IssueTest < ActiveSupport::TestCase
21 fixtures :projects, :users, :members, :member_roles, :roles,
21 fixtures :projects, :users, :members, :member_roles, :roles,
22 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :versions,
23 :versions,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
24 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :enumerations,
25 :enumerations,
26 :issues,
26 :issues,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
27 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :time_entries
28 :time_entries
29
29
30 def test_create
30 def test_create
31 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
31 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
32 assert issue.save
32 assert issue.save
33 issue.reload
33 issue.reload
34 assert_equal 1.5, issue.estimated_hours
34 assert_equal 1.5, issue.estimated_hours
35 end
35 end
36
36
37 def test_create_minimal
37 def test_create_minimal
38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
38 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
39 assert issue.save
39 assert issue.save
40 assert issue.description.nil?
40 assert issue.description.nil?
41 end
41 end
42
42
43 def test_create_with_required_custom_field
43 def test_create_with_required_custom_field
44 field = IssueCustomField.find_by_name('Database')
44 field = IssueCustomField.find_by_name('Database')
45 field.update_attribute(:is_required, true)
45 field.update_attribute(:is_required, true)
46
46
47 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
47 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
48 assert issue.available_custom_fields.include?(field)
48 assert issue.available_custom_fields.include?(field)
49 # No value for the custom field
49 # No value for the custom field
50 assert !issue.save
50 assert !issue.save
51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
51 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
52 # Blank value
52 # Blank value
53 issue.custom_field_values = { field.id => '' }
53 issue.custom_field_values = { field.id => '' }
54 assert !issue.save
54 assert !issue.save
55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
55 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
56 # Invalid value
56 # Invalid value
57 issue.custom_field_values = { field.id => 'SQLServer' }
57 issue.custom_field_values = { field.id => 'SQLServer' }
58 assert !issue.save
58 assert !issue.save
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
60 # Valid value
60 # Valid value
61 issue.custom_field_values = { field.id => 'PostgreSQL' }
61 issue.custom_field_values = { field.id => 'PostgreSQL' }
62 assert issue.save
62 assert issue.save
63 issue.reload
63 issue.reload
64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
64 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
65 end
65 end
66
66
67 def test_visible_scope_for_anonymous
67 def test_visible_scope_for_anonymous
68 # Anonymous user should see issues of public projects only
68 # Anonymous user should see issues of public projects only
69 issues = Issue.visible(User.anonymous).all
69 issues = Issue.visible(User.anonymous).all
70 assert issues.any?
70 assert issues.any?
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
71 assert_nil issues.detect {|issue| !issue.project.is_public?}
72 # Anonymous user should not see issues without permission
72 # Anonymous user should not see issues without permission
73 Role.anonymous.remove_permission!(:view_issues)
73 Role.anonymous.remove_permission!(:view_issues)
74 issues = Issue.visible(User.anonymous).all
74 issues = Issue.visible(User.anonymous).all
75 assert issues.empty?
75 assert issues.empty?
76 end
76 end
77
77
78 def test_visible_scope_for_user
78 def test_visible_scope_for_user
79 user = User.find(9)
79 user = User.find(9)
80 assert user.projects.empty?
80 assert user.projects.empty?
81 # Non member user should see issues of public projects only
81 # Non member user should see issues of public projects only
82 issues = Issue.visible(user).all
82 issues = Issue.visible(user).all
83 assert issues.any?
83 assert issues.any?
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
84 assert_nil issues.detect {|issue| !issue.project.is_public?}
85 # Non member user should not see issues without permission
85 # Non member user should not see issues without permission
86 Role.non_member.remove_permission!(:view_issues)
86 Role.non_member.remove_permission!(:view_issues)
87 user.reload
87 user.reload
88 issues = Issue.visible(user).all
88 issues = Issue.visible(user).all
89 assert issues.empty?
89 assert issues.empty?
90 # User should see issues of projects for which he has view_issues permissions only
90 # User should see issues of projects for which he has view_issues permissions only
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
91 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
92 user.reload
92 user.reload
93 issues = Issue.visible(user).all
93 issues = Issue.visible(user).all
94 assert issues.any?
94 assert issues.any?
95 assert_nil issues.detect {|issue| issue.project_id != 2}
95 assert_nil issues.detect {|issue| issue.project_id != 2}
96 end
96 end
97
97
98 def test_visible_scope_for_admin
98 def test_visible_scope_for_admin
99 user = User.find(1)
99 user = User.find(1)
100 user.members.each(&:destroy)
100 user.members.each(&:destroy)
101 assert user.projects.empty?
101 assert user.projects.empty?
102 issues = Issue.visible(user).all
102 issues = Issue.visible(user).all
103 assert issues.any?
103 assert issues.any?
104 # Admin should see issues on private projects that he does not belong to
104 # Admin should see issues on private projects that he does not belong to
105 assert issues.detect {|issue| !issue.project.is_public?}
105 assert issues.detect {|issue| !issue.project.is_public?}
106 end
106 end
107
107
108 def test_errors_full_messages_should_include_custom_fields_errors
108 def test_errors_full_messages_should_include_custom_fields_errors
109 field = IssueCustomField.find_by_name('Database')
109 field = IssueCustomField.find_by_name('Database')
110
110
111 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
111 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'test_create', :description => 'IssueTest#test_create_with_required_custom_field')
112 assert issue.available_custom_fields.include?(field)
112 assert issue.available_custom_fields.include?(field)
113 # Invalid value
113 # Invalid value
114 issue.custom_field_values = { field.id => 'SQLServer' }
114 issue.custom_field_values = { field.id => 'SQLServer' }
115
115
116 assert !issue.valid?
116 assert !issue.valid?
117 assert_equal 1, issue.errors.full_messages.size
117 assert_equal 1, issue.errors.full_messages.size
118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
118 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
119 end
119 end
120
120
121 def test_update_issue_with_required_custom_field
121 def test_update_issue_with_required_custom_field
122 field = IssueCustomField.find_by_name('Database')
122 field = IssueCustomField.find_by_name('Database')
123 field.update_attribute(:is_required, true)
123 field.update_attribute(:is_required, true)
124
124
125 issue = Issue.find(1)
125 issue = Issue.find(1)
126 assert_nil issue.custom_value_for(field)
126 assert_nil issue.custom_value_for(field)
127 assert issue.available_custom_fields.include?(field)
127 assert issue.available_custom_fields.include?(field)
128 # No change to custom values, issue can be saved
128 # No change to custom values, issue can be saved
129 assert issue.save
129 assert issue.save
130 # Blank value
130 # Blank value
131 issue.custom_field_values = { field.id => '' }
131 issue.custom_field_values = { field.id => '' }
132 assert !issue.save
132 assert !issue.save
133 # Valid value
133 # Valid value
134 issue.custom_field_values = { field.id => 'PostgreSQL' }
134 issue.custom_field_values = { field.id => 'PostgreSQL' }
135 assert issue.save
135 assert issue.save
136 issue.reload
136 issue.reload
137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
137 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
138 end
138 end
139
139
140 def test_should_not_update_attributes_if_custom_fields_validation_fails
140 def test_should_not_update_attributes_if_custom_fields_validation_fails
141 issue = Issue.find(1)
141 issue = Issue.find(1)
142 field = IssueCustomField.find_by_name('Database')
142 field = IssueCustomField.find_by_name('Database')
143 assert issue.available_custom_fields.include?(field)
143 assert issue.available_custom_fields.include?(field)
144
144
145 issue.custom_field_values = { field.id => 'Invalid' }
145 issue.custom_field_values = { field.id => 'Invalid' }
146 issue.subject = 'Should be not be saved'
146 issue.subject = 'Should be not be saved'
147 assert !issue.save
147 assert !issue.save
148
148
149 issue.reload
149 issue.reload
150 assert_equal "Can't print recipes", issue.subject
150 assert_equal "Can't print recipes", issue.subject
151 end
151 end
152
152
153 def test_should_not_recreate_custom_values_objects_on_update
153 def test_should_not_recreate_custom_values_objects_on_update
154 field = IssueCustomField.find_by_name('Database')
154 field = IssueCustomField.find_by_name('Database')
155
155
156 issue = Issue.find(1)
156 issue = Issue.find(1)
157 issue.custom_field_values = { field.id => 'PostgreSQL' }
157 issue.custom_field_values = { field.id => 'PostgreSQL' }
158 assert issue.save
158 assert issue.save
159 custom_value = issue.custom_value_for(field)
159 custom_value = issue.custom_value_for(field)
160 issue.reload
160 issue.reload
161 issue.custom_field_values = { field.id => 'MySQL' }
161 issue.custom_field_values = { field.id => 'MySQL' }
162 assert issue.save
162 assert issue.save
163 issue.reload
163 issue.reload
164 assert_equal custom_value.id, issue.custom_value_for(field).id
164 assert_equal custom_value.id, issue.custom_value_for(field).id
165 end
165 end
166
166
167 def test_should_update_issue_with_disabled_tracker
167 def test_should_update_issue_with_disabled_tracker
168 p = Project.find(1)
168 p = Project.find(1)
169 issue = Issue.find(1)
169 issue = Issue.find(1)
170
170
171 p.trackers.delete(issue.tracker)
171 p.trackers.delete(issue.tracker)
172 assert !p.trackers.include?(issue.tracker)
172 assert !p.trackers.include?(issue.tracker)
173
173
174 issue.reload
174 issue.reload
175 issue.subject = 'New subject'
175 issue.subject = 'New subject'
176 assert issue.save
176 assert issue.save
177 end
177 end
178
178
179 def test_should_not_set_a_disabled_tracker
179 def test_should_not_set_a_disabled_tracker
180 p = Project.find(1)
180 p = Project.find(1)
181 p.trackers.delete(Tracker.find(2))
181 p.trackers.delete(Tracker.find(2))
182
182
183 issue = Issue.find(1)
183 issue = Issue.find(1)
184 issue.tracker_id = 2
184 issue.tracker_id = 2
185 issue.subject = 'New subject'
185 issue.subject = 'New subject'
186 assert !issue.save
186 assert !issue.save
187 assert_not_nil issue.errors.on(:tracker_id)
187 assert_not_nil issue.errors.on(:tracker_id)
188 end
188 end
189
189
190 def test_category_based_assignment
190 def test_category_based_assignment
191 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
191 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Assignment test', :description => 'Assignment test', :category_id => 1)
192 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
192 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
193 end
193 end
194
194
195 def test_copy
195 def test_copy
196 issue = Issue.new.copy_from(1)
196 issue = Issue.new.copy_from(1)
197 assert issue.save
197 assert issue.save
198 issue.reload
198 issue.reload
199 orig = Issue.find(1)
199 orig = Issue.find(1)
200 assert_equal orig.subject, issue.subject
200 assert_equal orig.subject, issue.subject
201 assert_equal orig.tracker, issue.tracker
201 assert_equal orig.tracker, issue.tracker
202 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
202 assert_equal orig.custom_values.first.value, issue.custom_values.first.value
203 end
203 end
204
204
205 def test_copy_should_copy_status
205 def test_copy_should_copy_status
206 orig = Issue.find(8)
206 orig = Issue.find(8)
207 assert orig.status != IssueStatus.default
207 assert orig.status != IssueStatus.default
208
208
209 issue = Issue.new.copy_from(orig)
209 issue = Issue.new.copy_from(orig)
210 assert issue.save
210 assert issue.save
211 issue.reload
211 issue.reload
212 assert_equal orig.status, issue.status
212 assert_equal orig.status, issue.status
213 end
213 end
214
214
215 def test_should_close_duplicates
215 def test_should_close_duplicates
216 # Create 3 issues
216 # Create 3 issues
217 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
217 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
218 assert issue1.save
218 assert issue1.save
219 issue2 = issue1.clone
219 issue2 = issue1.clone
220 assert issue2.save
220 assert issue2.save
221 issue3 = issue1.clone
221 issue3 = issue1.clone
222 assert issue3.save
222 assert issue3.save
223
223
224 # 2 is a dupe of 1
224 # 2 is a dupe of 1
225 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
225 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
226 # And 3 is a dupe of 2
226 # And 3 is a dupe of 2
227 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
227 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
228 # And 3 is a dupe of 1 (circular duplicates)
228 # And 3 is a dupe of 1 (circular duplicates)
229 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
229 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
230
230
231 assert issue1.reload.duplicates.include?(issue2)
231 assert issue1.reload.duplicates.include?(issue2)
232
232
233 # Closing issue 1
233 # Closing issue 1
234 issue1.init_journal(User.find(:first), "Closing issue1")
234 issue1.init_journal(User.find(:first), "Closing issue1")
235 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
235 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
236 assert issue1.save
236 assert issue1.save
237 # 2 and 3 should be also closed
237 # 2 and 3 should be also closed
238 assert issue2.reload.closed?
238 assert issue2.reload.closed?
239 assert issue3.reload.closed?
239 assert issue3.reload.closed?
240 end
240 end
241
241
242 def test_should_not_close_duplicated_issue
242 def test_should_not_close_duplicated_issue
243 # Create 3 issues
243 # Create 3 issues
244 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
244 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'Duplicates test', :description => 'Duplicates test')
245 assert issue1.save
245 assert issue1.save
246 issue2 = issue1.clone
246 issue2 = issue1.clone
247 assert issue2.save
247 assert issue2.save
248
248
249 # 2 is a dupe of 1
249 # 2 is a dupe of 1
250 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
250 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
251 # 2 is a dup of 1 but 1 is not a duplicate of 2
251 # 2 is a dup of 1 but 1 is not a duplicate of 2
252 assert !issue2.reload.duplicates.include?(issue1)
252 assert !issue2.reload.duplicates.include?(issue1)
253
253
254 # Closing issue 2
254 # Closing issue 2
255 issue2.init_journal(User.find(:first), "Closing issue2")
255 issue2.init_journal(User.find(:first), "Closing issue2")
256 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
256 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
257 assert issue2.save
257 assert issue2.save
258 # 1 should not be also closed
258 # 1 should not be also closed
259 assert !issue1.reload.closed?
259 assert !issue1.reload.closed?
260 end
260 end
261
261
262 def test_assignable_versions
262 def test_assignable_versions
263 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
263 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
264 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
264 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
265 end
265 end
266
266
267 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
267 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
268 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
268 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
269 assert !issue.save
269 assert !issue.save
270 assert_not_nil issue.errors.on(:fixed_version_id)
270 assert_not_nil issue.errors.on(:fixed_version_id)
271 end
271 end
272
272
273 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
273 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
274 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
274 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
275 assert !issue.save
275 assert !issue.save
276 assert_not_nil issue.errors.on(:fixed_version_id)
276 assert_not_nil issue.errors.on(:fixed_version_id)
277 end
277 end
278
278
279 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
279 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
280 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
280 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
281 assert issue.save
281 assert issue.save
282 end
282 end
283
283
284 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
284 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
285 issue = Issue.find(11)
285 issue = Issue.find(11)
286 assert_equal 'closed', issue.fixed_version.status
286 assert_equal 'closed', issue.fixed_version.status
287 issue.subject = 'Subject changed'
287 issue.subject = 'Subject changed'
288 assert issue.save
288 assert issue.save
289 end
289 end
290
290
291 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
291 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
292 issue = Issue.find(11)
292 issue = Issue.find(11)
293 issue.status_id = 1
293 issue.status_id = 1
294 assert !issue.save
294 assert !issue.save
295 assert_not_nil issue.errors.on_base
295 assert_not_nil issue.errors.on_base
296 end
296 end
297
297
298 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
298 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
299 issue = Issue.find(11)
299 issue = Issue.find(11)
300 issue.status_id = 1
300 issue.status_id = 1
301 issue.fixed_version_id = 3
301 issue.fixed_version_id = 3
302 assert issue.save
302 assert issue.save
303 end
303 end
304
304
305 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
305 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
306 issue = Issue.find(12)
306 issue = Issue.find(12)
307 assert_equal 'locked', issue.fixed_version.status
307 assert_equal 'locked', issue.fixed_version.status
308 issue.status_id = 1
308 issue.status_id = 1
309 assert issue.save
309 assert issue.save
310 end
310 end
311
311
312 def test_move_to_another_project_with_same_category
312 def test_move_to_another_project_with_same_category
313 issue = Issue.find(1)
313 issue = Issue.find(1)
314 assert issue.move_to(Project.find(2))
314 assert issue.move_to(Project.find(2))
315 issue.reload
315 issue.reload
316 assert_equal 2, issue.project_id
316 assert_equal 2, issue.project_id
317 # Category changes
317 # Category changes
318 assert_equal 4, issue.category_id
318 assert_equal 4, issue.category_id
319 # Make sure time entries were move to the target project
319 # Make sure time entries were move to the target project
320 assert_equal 2, issue.time_entries.first.project_id
320 assert_equal 2, issue.time_entries.first.project_id
321 end
321 end
322
322
323 def test_move_to_another_project_without_same_category
323 def test_move_to_another_project_without_same_category
324 issue = Issue.find(2)
324 issue = Issue.find(2)
325 assert issue.move_to(Project.find(2))
325 assert issue.move_to(Project.find(2))
326 issue.reload
326 issue.reload
327 assert_equal 2, issue.project_id
327 assert_equal 2, issue.project_id
328 # Category cleared
328 # Category cleared
329 assert_nil issue.category_id
329 assert_nil issue.category_id
330 end
330 end
331
331
332 def test_copy_to_the_same_project
332 def test_copy_to_the_same_project
333 issue = Issue.find(1)
333 issue = Issue.find(1)
334 copy = nil
334 copy = nil
335 assert_difference 'Issue.count' do
335 assert_difference 'Issue.count' do
336 copy = issue.move_to(issue.project, nil, :copy => true)
336 copy = issue.move_to(issue.project, nil, :copy => true)
337 end
337 end
338 assert_kind_of Issue, copy
338 assert_kind_of Issue, copy
339 assert_equal issue.project, copy.project
339 assert_equal issue.project, copy.project
340 assert_equal "125", copy.custom_value_for(2).value
340 assert_equal "125", copy.custom_value_for(2).value
341 end
341 end
342
342
343 def test_copy_to_another_project_and_tracker
343 def test_copy_to_another_project_and_tracker
344 issue = Issue.find(1)
344 issue = Issue.find(1)
345 copy = nil
345 copy = nil
346 assert_difference 'Issue.count' do
346 assert_difference 'Issue.count' do
347 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
347 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
348 end
348 end
349 assert_kind_of Issue, copy
349 assert_kind_of Issue, copy
350 assert_equal Project.find(3), copy.project
350 assert_equal Project.find(3), copy.project
351 assert_equal Tracker.find(2), copy.tracker
351 assert_equal Tracker.find(2), copy.tracker
352 # Custom field #2 is not associated with target tracker
352 # Custom field #2 is not associated with target tracker
353 assert_nil copy.custom_value_for(2)
353 assert_nil copy.custom_value_for(2)
354 end
354 end
355
355
356 def test_recipients_should_not_include_users_that_cannot_view_the_issue
357 issue = Issue.find(12)
358 assert issue.recipients.include?(issue.author.mail)
359 # move the issue to a private project
360 copy = issue.move_to(Project.find(5), Tracker.find(2), :copy => true)
361 # author is not a member of project anymore
362 assert !copy.recipients.include?(copy.author.mail)
363 end
364
365 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
366 user = User.find(3)
367 issue = Issue.find(9)
368 Watcher.create!(:user => user, :watchable => issue)
369 assert issue.watched_by?(user)
370 assert !issue.watcher_recipients.include?(user.mail)
371 end
372
356 def test_issue_destroy
373 def test_issue_destroy
357 Issue.find(1).destroy
374 Issue.find(1).destroy
358 assert_nil Issue.find_by_id(1)
375 assert_nil Issue.find_by_id(1)
359 assert_nil TimeEntry.find_by_issue_id(1)
376 assert_nil TimeEntry.find_by_issue_id(1)
360 end
377 end
361
378
362 def test_blocked
379 def test_blocked
363 blocked_issue = Issue.find(9)
380 blocked_issue = Issue.find(9)
364 blocking_issue = Issue.find(10)
381 blocking_issue = Issue.find(10)
365
382
366 assert blocked_issue.blocked?
383 assert blocked_issue.blocked?
367 assert !blocking_issue.blocked?
384 assert !blocking_issue.blocked?
368 end
385 end
369
386
370 def test_blocked_issues_dont_allow_closed_statuses
387 def test_blocked_issues_dont_allow_closed_statuses
371 blocked_issue = Issue.find(9)
388 blocked_issue = Issue.find(9)
372
389
373 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
390 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
374 assert !allowed_statuses.empty?
391 assert !allowed_statuses.empty?
375 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
392 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
376 assert closed_statuses.empty?
393 assert closed_statuses.empty?
377 end
394 end
378
395
379 def test_unblocked_issues_allow_closed_statuses
396 def test_unblocked_issues_allow_closed_statuses
380 blocking_issue = Issue.find(10)
397 blocking_issue = Issue.find(10)
381
398
382 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
399 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
383 assert !allowed_statuses.empty?
400 assert !allowed_statuses.empty?
384 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
401 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
385 assert !closed_statuses.empty?
402 assert !closed_statuses.empty?
386 end
403 end
387
404
388 def test_overdue
405 def test_overdue
389 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
406 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
390 assert !Issue.new(:due_date => Date.today).overdue?
407 assert !Issue.new(:due_date => Date.today).overdue?
391 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
408 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
392 assert !Issue.new(:due_date => nil).overdue?
409 assert !Issue.new(:due_date => nil).overdue?
393 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
410 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
394 end
411 end
395
412
396 def test_assignable_users
413 def test_assignable_users
397 assert_kind_of User, Issue.find(1).assignable_users.first
414 assert_kind_of User, Issue.find(1).assignable_users.first
398 end
415 end
399
416
400 def test_create_should_send_email_notification
417 def test_create_should_send_email_notification
401 ActionMailer::Base.deliveries.clear
418 ActionMailer::Base.deliveries.clear
402 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
419 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :estimated_hours => '1:30')
403
420
404 assert issue.save
421 assert issue.save
405 assert_equal 1, ActionMailer::Base.deliveries.size
422 assert_equal 1, ActionMailer::Base.deliveries.size
406 end
423 end
407
424
408 def test_stale_issue_should_not_send_email_notification
425 def test_stale_issue_should_not_send_email_notification
409 ActionMailer::Base.deliveries.clear
426 ActionMailer::Base.deliveries.clear
410 issue = Issue.find(1)
427 issue = Issue.find(1)
411 stale = Issue.find(1)
428 stale = Issue.find(1)
412
429
413 issue.init_journal(User.find(1))
430 issue.init_journal(User.find(1))
414 issue.subject = 'Subjet update'
431 issue.subject = 'Subjet update'
415 assert issue.save
432 assert issue.save
416 assert_equal 1, ActionMailer::Base.deliveries.size
433 assert_equal 1, ActionMailer::Base.deliveries.size
417 ActionMailer::Base.deliveries.clear
434 ActionMailer::Base.deliveries.clear
418
435
419 stale.init_journal(User.find(1))
436 stale.init_journal(User.find(1))
420 stale.subject = 'Another subjet update'
437 stale.subject = 'Another subjet update'
421 assert_raise ActiveRecord::StaleObjectError do
438 assert_raise ActiveRecord::StaleObjectError do
422 stale.save
439 stale.save
423 end
440 end
424 assert ActionMailer::Base.deliveries.empty?
441 assert ActionMailer::Base.deliveries.empty?
425 end
442 end
426 end
443 end
General Comments 0
You need to be logged in to leave comments. Login now