##// END OF EJS Templates
Adds named scopes to replace custom finders....
Eric Davis -
r3443:22c1e2b8cf85
parent child
Show More
@@ -1,608 +1,614
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Issue < ActiveRecord::Base
19 19 belongs_to :project
20 20 belongs_to :tracker
21 21 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
22 22 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
23 23 belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
24 24 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
25 25 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
26 26 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
27 27
28 28 has_many :journals, :as => :journalized, :dependent => :destroy
29 29 has_many :time_entries, :dependent => :delete_all
30 30 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
31 31
32 32 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
33 33 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
34 34
35 35 acts_as_attachable :after_remove => :attachment_removed
36 36 acts_as_customizable
37 37 acts_as_watchable
38 38 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
39 39 :include => [:project, :journals],
40 40 # sort by id so that limited eager loading doesn't break with postgresql
41 41 :order_column => "#{table_name}.id"
42 42 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
43 43 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
44 44 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
45 45
46 46 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
47 47 :author_key => :author_id
48 48
49 49 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
50 50
51 51 attr_reader :current_journal
52
52
53 53 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
54
54 55 validates_length_of :subject, :maximum => 255
55 56 validates_inclusion_of :done_ratio, :in => 0..100
56 57 validates_numericality_of :estimated_hours, :allow_nil => true
57 58
58 59 named_scope :visible, lambda {|*args| { :include => :project,
59 60 :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
60 61
61 62 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
62 63
64 named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
65 named_scope :with_limit, lambda { |limit| { :limit => limit} }
66 named_scope :on_active_project, :include => [:status, :project, :tracker],
67 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
68
63 69 before_create :default_assign
64 70 before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
65 71 after_save :create_journal
66 72
67 73 # Returns true if usr or current user is allowed to view the issue
68 74 def visible?(usr=nil)
69 75 (usr || User.current).allowed_to?(:view_issues, self.project)
70 76 end
71 77
72 78 def after_initialize
73 79 if new_record?
74 80 # set default values for new records only
75 81 self.status ||= IssueStatus.default
76 82 self.priority ||= IssuePriority.default
77 83 end
78 84 end
79 85
80 86 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
81 87 def available_custom_fields
82 88 (project && tracker) ? project.all_issue_custom_fields.select {|c| tracker.custom_fields.include? c } : []
83 89 end
84 90
85 91 def copy_from(arg)
86 92 issue = arg.is_a?(Issue) ? arg : Issue.find(arg)
87 93 self.attributes = issue.attributes.dup.except("id", "created_on", "updated_on")
88 94 self.custom_values = issue.custom_values.collect {|v| v.clone}
89 95 self.status = issue.status
90 96 self
91 97 end
92 98
93 99 # Moves/copies an issue to a new project and tracker
94 100 # Returns the moved/copied issue on success, false on failure
95 101 def move_to(new_project, new_tracker = nil, options = {})
96 102 options ||= {}
97 103 issue = options[:copy] ? self.clone : self
98 104 transaction do
99 105 if new_project && issue.project_id != new_project.id
100 106 # delete issue relations
101 107 unless Setting.cross_project_issue_relations?
102 108 issue.relations_from.clear
103 109 issue.relations_to.clear
104 110 end
105 111 # issue is moved to another project
106 112 # reassign to the category with same name if any
107 113 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
108 114 issue.category = new_category
109 115 # Keep the fixed_version if it's still valid in the new_project
110 116 unless new_project.shared_versions.include?(issue.fixed_version)
111 117 issue.fixed_version = nil
112 118 end
113 119 issue.project = new_project
114 120 end
115 121 if new_tracker
116 122 issue.tracker = new_tracker
117 123 end
118 124 if options[:copy]
119 125 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
120 126 issue.status = if options[:attributes] && options[:attributes][:status_id]
121 127 IssueStatus.find_by_id(options[:attributes][:status_id])
122 128 else
123 129 self.status
124 130 end
125 131 end
126 132 # Allow bulk setting of attributes on the issue
127 133 if options[:attributes]
128 134 issue.attributes = options[:attributes]
129 135 end
130 136 if issue.save
131 137 unless options[:copy]
132 138 # Manually update project_id on related time entries
133 139 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
134 140 end
135 141 else
136 142 Issue.connection.rollback_db_transaction
137 143 return false
138 144 end
139 145 end
140 146 return issue
141 147 end
142 148
143 149 def priority_id=(pid)
144 150 self.priority = nil
145 151 write_attribute(:priority_id, pid)
146 152 end
147 153
148 154 def tracker_id=(tid)
149 155 self.tracker = nil
150 156 result = write_attribute(:tracker_id, tid)
151 157 @custom_field_values = nil
152 158 result
153 159 end
154 160
155 161 # Overrides attributes= so that tracker_id gets assigned first
156 162 def attributes_with_tracker_first=(new_attributes, *args)
157 163 return if new_attributes.nil?
158 164 new_tracker_id = new_attributes['tracker_id'] || new_attributes[:tracker_id]
159 165 if new_tracker_id
160 166 self.tracker_id = new_tracker_id
161 167 end
162 168 send :attributes_without_tracker_first=, new_attributes, *args
163 169 end
164 170 # Do not redefine alias chain on reload (see #4838)
165 171 alias_method_chain(:attributes=, :tracker_first) unless method_defined?(:attributes_without_tracker_first=)
166 172
167 173 def estimated_hours=(h)
168 174 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
169 175 end
170 176
171 177 SAFE_ATTRIBUTES = %w(
172 178 tracker_id
173 179 status_id
174 180 category_id
175 181 assigned_to_id
176 182 priority_id
177 183 fixed_version_id
178 184 subject
179 185 description
180 186 start_date
181 187 due_date
182 188 done_ratio
183 189 estimated_hours
184 190 custom_field_values
185 191 ) unless const_defined?(:SAFE_ATTRIBUTES)
186 192
187 193 # Safely sets attributes
188 194 # Should be called from controllers instead of #attributes=
189 195 # attr_accessible is too rough because we still want things like
190 196 # Issue.new(:project => foo) to work
191 197 # TODO: move workflow/permission checks from controllers to here
192 198 def safe_attributes=(attrs, user=User.current)
193 199 return if attrs.nil?
194 200 attrs = attrs.reject {|k,v| !SAFE_ATTRIBUTES.include?(k)}
195 201 if attrs['status_id']
196 202 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
197 203 attrs.delete('status_id')
198 204 end
199 205 end
200 206 self.attributes = attrs
201 207 end
202 208
203 209 def done_ratio
204 210 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
205 211 status.default_done_ratio
206 212 else
207 213 read_attribute(:done_ratio)
208 214 end
209 215 end
210 216
211 217 def self.use_status_for_done_ratio?
212 218 Setting.issue_done_ratio == 'issue_status'
213 219 end
214 220
215 221 def self.use_field_for_done_ratio?
216 222 Setting.issue_done_ratio == 'issue_field'
217 223 end
218 224
219 225 def validate
220 226 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
221 227 errors.add :due_date, :not_a_date
222 228 end
223 229
224 230 if self.due_date and self.start_date and self.due_date < self.start_date
225 231 errors.add :due_date, :greater_than_start_date
226 232 end
227 233
228 234 if start_date && soonest_start && start_date < soonest_start
229 235 errors.add :start_date, :invalid
230 236 end
231 237
232 238 if fixed_version
233 239 if !assignable_versions.include?(fixed_version)
234 240 errors.add :fixed_version_id, :inclusion
235 241 elsif reopened? && fixed_version.closed?
236 242 errors.add_to_base I18n.t(:error_can_not_reopen_issue_on_closed_version)
237 243 end
238 244 end
239 245
240 246 # Checks that the issue can not be added/moved to a disabled tracker
241 247 if project && (tracker_id_changed? || project_id_changed?)
242 248 unless project.trackers.include?(tracker)
243 249 errors.add :tracker_id, :inclusion
244 250 end
245 251 end
246 252 end
247 253
248 254 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
249 255 # even if the user turns off the setting later
250 256 def update_done_ratio_from_issue_status
251 257 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
252 258 self.done_ratio = status.default_done_ratio
253 259 end
254 260 end
255 261
256 262 def init_journal(user, notes = "")
257 263 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
258 264 @issue_before_change = self.clone
259 265 @issue_before_change.status = self.status
260 266 @custom_values_before_change = {}
261 267 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
262 268 # Make sure updated_on is updated when adding a note.
263 269 updated_on_will_change!
264 270 @current_journal
265 271 end
266 272
267 273 # Return true if the issue is closed, otherwise false
268 274 def closed?
269 275 self.status.is_closed?
270 276 end
271 277
272 278 # Return true if the issue is being reopened
273 279 def reopened?
274 280 if !new_record? && status_id_changed?
275 281 status_was = IssueStatus.find_by_id(status_id_was)
276 282 status_new = IssueStatus.find_by_id(status_id)
277 283 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
278 284 return true
279 285 end
280 286 end
281 287 false
282 288 end
283 289
284 290 # Return true if the issue is being closed
285 291 def closing?
286 292 if !new_record? && status_id_changed?
287 293 status_was = IssueStatus.find_by_id(status_id_was)
288 294 status_new = IssueStatus.find_by_id(status_id)
289 295 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
290 296 return true
291 297 end
292 298 end
293 299 false
294 300 end
295 301
296 302 # Returns true if the issue is overdue
297 303 def overdue?
298 304 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
299 305 end
300 306
301 307 # Users the issue can be assigned to
302 308 def assignable_users
303 309 project.assignable_users
304 310 end
305 311
306 312 # Versions that the issue can be assigned to
307 313 def assignable_versions
308 314 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
309 315 end
310 316
311 317 # Returns true if this issue is blocked by another issue that is still open
312 318 def blocked?
313 319 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
314 320 end
315 321
316 322 # Returns an array of status that user is able to apply
317 323 def new_statuses_allowed_to(user, include_default=false)
318 324 statuses = status.find_new_statuses_allowed_to(user.roles_for_project(project), tracker)
319 325 statuses << status unless statuses.empty?
320 326 statuses << IssueStatus.default if include_default
321 327 statuses = statuses.uniq.sort
322 328 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
323 329 end
324 330
325 331 # Returns the mail adresses of users that should be notified
326 332 def recipients
327 333 notified = project.notified_users
328 334 # Author and assignee are always notified unless they have been locked
329 335 notified << author if author && author.active?
330 336 notified << assigned_to if assigned_to && assigned_to.active?
331 337 notified.uniq!
332 338 # Remove users that can not view the issue
333 339 notified.reject! {|user| !visible?(user)}
334 340 notified.collect(&:mail)
335 341 end
336 342
337 343 # Returns the total number of hours spent on this issue.
338 344 #
339 345 # Example:
340 346 # spent_hours => 0
341 347 # spent_hours => 50
342 348 def spent_hours
343 349 @spent_hours ||= time_entries.sum(:hours) || 0
344 350 end
345 351
346 352 def relations
347 353 (relations_from + relations_to).sort
348 354 end
349 355
350 356 def all_dependent_issues
351 357 dependencies = []
352 358 relations_from.each do |relation|
353 359 dependencies << relation.issue_to
354 360 dependencies += relation.issue_to.all_dependent_issues
355 361 end
356 362 dependencies
357 363 end
358 364
359 365 # Returns an array of issues that duplicate this one
360 366 def duplicates
361 367 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
362 368 end
363 369
364 370 # Returns the due date or the target due date if any
365 371 # Used on gantt chart
366 372 def due_before
367 373 due_date || (fixed_version ? fixed_version.effective_date : nil)
368 374 end
369 375
370 376 # Returns the time scheduled for this issue.
371 377 #
372 378 # Example:
373 379 # Start Date: 2/26/09, End Date: 3/04/09
374 380 # duration => 6
375 381 def duration
376 382 (start_date && due_date) ? due_date - start_date : 0
377 383 end
378 384
379 385 def soonest_start
380 386 @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
381 387 end
382 388
383 389 def to_s
384 390 "#{tracker} ##{id}: #{subject}"
385 391 end
386 392
387 393 # Returns a string of css classes that apply to the issue
388 394 def css_classes
389 395 s = "issue status-#{status.position} priority-#{priority.position}"
390 396 s << ' closed' if closed?
391 397 s << ' overdue' if overdue?
392 398 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
393 399 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
394 400 s
395 401 end
396 402
397 403 # Saves an issue, time_entry, attachments, and a journal from the parameters
398 404 def save_issue_with_child_records(params, existing_time_entry=nil)
399 405 if params[:time_entry] && params[:time_entry][:hours].present? && User.current.allowed_to?(:log_time, project)
400 406 @time_entry = existing_time_entry || TimeEntry.new
401 407 @time_entry.project = project
402 408 @time_entry.issue = self
403 409 @time_entry.user = User.current
404 410 @time_entry.spent_on = Date.today
405 411 @time_entry.attributes = params[:time_entry]
406 412 self.time_entries << @time_entry
407 413 end
408 414
409 415 if valid?
410 416 attachments = Attachment.attach_files(self, params[:attachments])
411 417
412 418 attachments[:files].each {|a| @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
413 419 # TODO: Rename hook
414 420 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
415 421 if save
416 422 # TODO: Rename hook
417 423 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
418 424 return true
419 425 end
420 426 end
421 427 # failure, returns false
422 428
423 429 end
424 430
425 431 # Unassigns issues from +version+ if it's no longer shared with issue's project
426 432 def self.update_versions_from_sharing_change(version)
427 433 # Update issues assigned to the version
428 434 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
429 435 end
430 436
431 437 # Unassigns issues from versions that are no longer shared
432 438 # after +project+ was moved
433 439 def self.update_versions_from_hierarchy_change(project)
434 440 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
435 441 # Update issues of the moved projects and issues assigned to a version of a moved project
436 442 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
437 443 end
438 444
439 445 # Extracted from the ReportsController.
440 446 def self.by_tracker(project)
441 447 count_and_group_by(:project => project,
442 448 :field => 'tracker_id',
443 449 :joins => Tracker.table_name)
444 450 end
445 451
446 452 def self.by_version(project)
447 453 count_and_group_by(:project => project,
448 454 :field => 'fixed_version_id',
449 455 :joins => Version.table_name)
450 456 end
451 457
452 458 def self.by_priority(project)
453 459 count_and_group_by(:project => project,
454 460 :field => 'priority_id',
455 461 :joins => IssuePriority.table_name)
456 462 end
457 463
458 464 def self.by_category(project)
459 465 count_and_group_by(:project => project,
460 466 :field => 'category_id',
461 467 :joins => IssueCategory.table_name)
462 468 end
463 469
464 470 def self.by_assigned_to(project)
465 471 count_and_group_by(:project => project,
466 472 :field => 'assigned_to_id',
467 473 :joins => User.table_name)
468 474 end
469 475
470 476 def self.by_author(project)
471 477 count_and_group_by(:project => project,
472 478 :field => 'author_id',
473 479 :joins => User.table_name)
474 480 end
475 481
476 482 def self.by_subproject(project)
477 483 ActiveRecord::Base.connection.select_all("select s.id as status_id,
478 484 s.is_closed as closed,
479 485 i.project_id as project_id,
480 486 count(i.id) as total
481 487 from
482 488 #{Issue.table_name} i, #{IssueStatus.table_name} s
483 489 where
484 490 i.status_id=s.id
485 491 and i.project_id IN (#{project.descendants.active.collect{|p| p.id}.join(',')})
486 492 group by s.id, s.is_closed, i.project_id") if project.descendants.active.any?
487 493 end
488 494 # End ReportsController extraction
489 495
490 496 private
491 497
492 498 # Update issues so their versions are not pointing to a
493 499 # fixed_version that is not shared with the issue's project
494 500 def self.update_versions(conditions=nil)
495 501 # Only need to update issues with a fixed_version from
496 502 # a different project and that is not systemwide shared
497 503 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
498 504 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
499 505 " AND #{Version.table_name}.sharing <> 'system'",
500 506 conditions),
501 507 :include => [:project, :fixed_version]
502 508 ).each do |issue|
503 509 next if issue.project.nil? || issue.fixed_version.nil?
504 510 unless issue.project.shared_versions.include?(issue.fixed_version)
505 511 issue.init_journal(User.current)
506 512 issue.fixed_version = nil
507 513 issue.save
508 514 end
509 515 end
510 516 end
511 517
512 518 # Callback on attachment deletion
513 519 def attachment_removed(obj)
514 520 journal = init_journal(User.current)
515 521 journal.details << JournalDetail.new(:property => 'attachment',
516 522 :prop_key => obj.id,
517 523 :old_value => obj.filename)
518 524 journal.save
519 525 end
520 526
521 527 # Default assignment based on category
522 528 def default_assign
523 529 if assigned_to.nil? && category && category.assigned_to
524 530 self.assigned_to = category.assigned_to
525 531 end
526 532 end
527 533
528 534 # Updates start/due dates of following issues
529 535 def reschedule_following_issues
530 536 if start_date_changed? || due_date_changed?
531 537 relations_from.each do |relation|
532 538 relation.set_issue_to_dates
533 539 end
534 540 end
535 541 end
536 542
537 543 # Closes duplicates if the issue is being closed
538 544 def close_duplicates
539 545 if closing?
540 546 duplicates.each do |duplicate|
541 547 # Reload is need in case the duplicate was updated by a previous duplicate
542 548 duplicate.reload
543 549 # Don't re-close it if it's already closed
544 550 next if duplicate.closed?
545 551 # Same user and notes
546 552 if @current_journal
547 553 duplicate.init_journal(@current_journal.user, @current_journal.notes)
548 554 end
549 555 duplicate.update_attribute :status, self.status
550 556 end
551 557 end
552 558 end
553 559
554 560 # Saves the changes in a Journal
555 561 # Called after_save
556 562 def create_journal
557 563 if @current_journal
558 564 # attributes changes
559 565 (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
560 566 @current_journal.details << JournalDetail.new(:property => 'attr',
561 567 :prop_key => c,
562 568 :old_value => @issue_before_change.send(c),
563 569 :value => send(c)) unless send(c)==@issue_before_change.send(c)
564 570 }
565 571 # custom fields changes
566 572 custom_values.each {|c|
567 573 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
568 574 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
569 575 @current_journal.details << JournalDetail.new(:property => 'cf',
570 576 :prop_key => c.custom_field_id,
571 577 :old_value => @custom_values_before_change[c.custom_field_id],
572 578 :value => c.value)
573 579 }
574 580 @current_journal.save
575 581 # reset current journal
576 582 init_journal @current_journal.user, @current_journal.notes
577 583 end
578 584 end
579 585
580 586 # Query generator for selecting groups of issue counts for a project
581 587 # based on specific criteria
582 588 #
583 589 # Options
584 590 # * project - Project to search in.
585 591 # * field - String. Issue field to key off of in the grouping.
586 592 # * joins - String. The table name to join against.
587 593 def self.count_and_group_by(options)
588 594 project = options.delete(:project)
589 595 select_field = options.delete(:field)
590 596 joins = options.delete(:joins)
591 597
592 598 where = "i.#{select_field}=j.id"
593 599
594 600 ActiveRecord::Base.connection.select_all("select s.id as status_id,
595 601 s.is_closed as closed,
596 602 j.id as #{select_field},
597 603 count(i.id) as total
598 604 from
599 605 #{Issue.table_name} i, #{IssueStatus.table_name} s, #{joins} as j
600 606 where
601 607 i.status_id=s.id
602 608 and #{where}
603 609 and i.project_id=#{project.id}
604 610 group by s.id, s.is_closed, j.id")
605 611 end
606 612
607 613
608 614 end
@@ -1,16 +1,11
1 <h3><%=l(:label_watched_issues)%> (<%= Issue.visible.count(:include => :watchers,
2 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id]) %>)</h3>
3 <% watched_issues = Issue.visible.find(:all,
4 :include => [:status, :project, :tracker, :watchers],
5 :limit => 10,
6 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id],
7 :order => "#{Issue.table_name}.updated_on DESC") %>
1 <h3><%=l(:label_watched_issues)%> (<%= Issue.visible.watched_by(user.id).count %>)</h3>
2 <% watched_issues = Issue.visible.on_active_project.watched_by(user.id).recently_updated.with_limit(10) %>
8 3
9 4 <%= render :partial => 'issues/list_simple', :locals => { :issues => watched_issues } %>
10 5 <% if watched_issues.length > 0 %>
11 6 <p class="small"><%= link_to l(:label_issue_view_all), :controller => 'issues',
12 7 :action => 'index',
13 8 :set_filter => 1,
14 9 :watcher_id => 'me',
15 10 :sort => 'updated_on:desc' %></p>
16 11 <% end %>
@@ -1,659 +1,679
1 1 # redMine - project management software
2 2 # Copyright (C) 2006-2007 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.dirname(__FILE__) + '/../test_helper'
19 19
20 20 class IssueTest < ActiveSupport::TestCase
21 21 fixtures :projects, :users, :members, :member_roles, :roles,
22 22 :trackers, :projects_trackers,
23 23 :enabled_modules,
24 24 :versions,
25 25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 26 :enumerations,
27 27 :issues,
28 28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 29 :time_entries
30 30
31 31 def test_create
32 32 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')
33 33 assert issue.save
34 34 issue.reload
35 35 assert_equal 1.5, issue.estimated_hours
36 36 end
37 37
38 38 def test_create_minimal
39 39 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create')
40 40 assert issue.save
41 41 assert issue.description.nil?
42 42 end
43 43
44 44 def test_create_with_required_custom_field
45 45 field = IssueCustomField.find_by_name('Database')
46 46 field.update_attribute(:is_required, true)
47 47
48 48 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')
49 49 assert issue.available_custom_fields.include?(field)
50 50 # No value for the custom field
51 51 assert !issue.save
52 52 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
53 53 # Blank value
54 54 issue.custom_field_values = { field.id => '' }
55 55 assert !issue.save
56 56 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
57 57 # Invalid value
58 58 issue.custom_field_values = { field.id => 'SQLServer' }
59 59 assert !issue.save
60 60 assert_equal I18n.translate('activerecord.errors.messages.invalid'), issue.errors.on(:custom_values)
61 61 # Valid value
62 62 issue.custom_field_values = { field.id => 'PostgreSQL' }
63 63 assert issue.save
64 64 issue.reload
65 65 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
66 66 end
67 67
68 68 def test_visible_scope_for_anonymous
69 69 # Anonymous user should see issues of public projects only
70 70 issues = Issue.visible(User.anonymous).all
71 71 assert issues.any?
72 72 assert_nil issues.detect {|issue| !issue.project.is_public?}
73 73 # Anonymous user should not see issues without permission
74 74 Role.anonymous.remove_permission!(:view_issues)
75 75 issues = Issue.visible(User.anonymous).all
76 76 assert issues.empty?
77 77 end
78 78
79 79 def test_visible_scope_for_user
80 80 user = User.find(9)
81 81 assert user.projects.empty?
82 82 # Non member user should see issues of public projects only
83 83 issues = Issue.visible(user).all
84 84 assert issues.any?
85 85 assert_nil issues.detect {|issue| !issue.project.is_public?}
86 86 # Non member user should not see issues without permission
87 87 Role.non_member.remove_permission!(:view_issues)
88 88 user.reload
89 89 issues = Issue.visible(user).all
90 90 assert issues.empty?
91 91 # User should see issues of projects for which he has view_issues permissions only
92 92 Member.create!(:principal => user, :project_id => 2, :role_ids => [1])
93 93 user.reload
94 94 issues = Issue.visible(user).all
95 95 assert issues.any?
96 96 assert_nil issues.detect {|issue| issue.project_id != 2}
97 97 end
98 98
99 99 def test_visible_scope_for_admin
100 100 user = User.find(1)
101 101 user.members.each(&:destroy)
102 102 assert user.projects.empty?
103 103 issues = Issue.visible(user).all
104 104 assert issues.any?
105 105 # Admin should see issues on private projects that he does not belong to
106 106 assert issues.detect {|issue| !issue.project.is_public?}
107 107 end
108 108
109 109 def test_errors_full_messages_should_include_custom_fields_errors
110 110 field = IssueCustomField.find_by_name('Database')
111 111
112 112 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')
113 113 assert issue.available_custom_fields.include?(field)
114 114 # Invalid value
115 115 issue.custom_field_values = { field.id => 'SQLServer' }
116 116
117 117 assert !issue.valid?
118 118 assert_equal 1, issue.errors.full_messages.size
119 119 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}", issue.errors.full_messages.first
120 120 end
121 121
122 122 def test_update_issue_with_required_custom_field
123 123 field = IssueCustomField.find_by_name('Database')
124 124 field.update_attribute(:is_required, true)
125 125
126 126 issue = Issue.find(1)
127 127 assert_nil issue.custom_value_for(field)
128 128 assert issue.available_custom_fields.include?(field)
129 129 # No change to custom values, issue can be saved
130 130 assert issue.save
131 131 # Blank value
132 132 issue.custom_field_values = { field.id => '' }
133 133 assert !issue.save
134 134 # Valid value
135 135 issue.custom_field_values = { field.id => 'PostgreSQL' }
136 136 assert issue.save
137 137 issue.reload
138 138 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
139 139 end
140 140
141 141 def test_should_not_update_attributes_if_custom_fields_validation_fails
142 142 issue = Issue.find(1)
143 143 field = IssueCustomField.find_by_name('Database')
144 144 assert issue.available_custom_fields.include?(field)
145 145
146 146 issue.custom_field_values = { field.id => 'Invalid' }
147 147 issue.subject = 'Should be not be saved'
148 148 assert !issue.save
149 149
150 150 issue.reload
151 151 assert_equal "Can't print recipes", issue.subject
152 152 end
153 153
154 154 def test_should_not_recreate_custom_values_objects_on_update
155 155 field = IssueCustomField.find_by_name('Database')
156 156
157 157 issue = Issue.find(1)
158 158 issue.custom_field_values = { field.id => 'PostgreSQL' }
159 159 assert issue.save
160 160 custom_value = issue.custom_value_for(field)
161 161 issue.reload
162 162 issue.custom_field_values = { field.id => 'MySQL' }
163 163 assert issue.save
164 164 issue.reload
165 165 assert_equal custom_value.id, issue.custom_value_for(field).id
166 166 end
167 167
168 168 def test_assigning_tracker_id_should_reload_custom_fields_values
169 169 issue = Issue.new(:project => Project.find(1))
170 170 assert issue.custom_field_values.empty?
171 171 issue.tracker_id = 1
172 172 assert issue.custom_field_values.any?
173 173 end
174 174
175 175 def test_assigning_attributes_should_assign_tracker_id_first
176 176 attributes = ActiveSupport::OrderedHash.new
177 177 attributes['custom_field_values'] = { '1' => 'MySQL' }
178 178 attributes['tracker_id'] = '1'
179 179 issue = Issue.new(:project => Project.find(1))
180 180 issue.attributes = attributes
181 181 assert_not_nil issue.custom_value_for(1)
182 182 assert_equal 'MySQL', issue.custom_value_for(1).value
183 183 end
184 184
185 185 def test_should_update_issue_with_disabled_tracker
186 186 p = Project.find(1)
187 187 issue = Issue.find(1)
188 188
189 189 p.trackers.delete(issue.tracker)
190 190 assert !p.trackers.include?(issue.tracker)
191 191
192 192 issue.reload
193 193 issue.subject = 'New subject'
194 194 assert issue.save
195 195 end
196 196
197 197 def test_should_not_set_a_disabled_tracker
198 198 p = Project.find(1)
199 199 p.trackers.delete(Tracker.find(2))
200 200
201 201 issue = Issue.find(1)
202 202 issue.tracker_id = 2
203 203 issue.subject = 'New subject'
204 204 assert !issue.save
205 205 assert_not_nil issue.errors.on(:tracker_id)
206 206 end
207 207
208 208 def test_category_based_assignment
209 209 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)
210 210 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
211 211 end
212 212
213 213 def test_copy
214 214 issue = Issue.new.copy_from(1)
215 215 assert issue.save
216 216 issue.reload
217 217 orig = Issue.find(1)
218 218 assert_equal orig.subject, issue.subject
219 219 assert_equal orig.tracker, issue.tracker
220 220 assert_equal "125", issue.custom_value_for(2).value
221 221 end
222 222
223 223 def test_copy_should_copy_status
224 224 orig = Issue.find(8)
225 225 assert orig.status != IssueStatus.default
226 226
227 227 issue = Issue.new.copy_from(orig)
228 228 assert issue.save
229 229 issue.reload
230 230 assert_equal orig.status, issue.status
231 231 end
232 232
233 233 def test_should_close_duplicates
234 234 # Create 3 issues
235 235 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')
236 236 assert issue1.save
237 237 issue2 = issue1.clone
238 238 assert issue2.save
239 239 issue3 = issue1.clone
240 240 assert issue3.save
241 241
242 242 # 2 is a dupe of 1
243 243 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
244 244 # And 3 is a dupe of 2
245 245 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
246 246 # And 3 is a dupe of 1 (circular duplicates)
247 247 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
248 248
249 249 assert issue1.reload.duplicates.include?(issue2)
250 250
251 251 # Closing issue 1
252 252 issue1.init_journal(User.find(:first), "Closing issue1")
253 253 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
254 254 assert issue1.save
255 255 # 2 and 3 should be also closed
256 256 assert issue2.reload.closed?
257 257 assert issue3.reload.closed?
258 258 end
259 259
260 260 def test_should_not_close_duplicated_issue
261 261 # Create 3 issues
262 262 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')
263 263 assert issue1.save
264 264 issue2 = issue1.clone
265 265 assert issue2.save
266 266
267 267 # 2 is a dupe of 1
268 268 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
269 269 # 2 is a dup of 1 but 1 is not a duplicate of 2
270 270 assert !issue2.reload.duplicates.include?(issue1)
271 271
272 272 # Closing issue 2
273 273 issue2.init_journal(User.find(:first), "Closing issue2")
274 274 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
275 275 assert issue2.save
276 276 # 1 should not be also closed
277 277 assert !issue1.reload.closed?
278 278 end
279 279
280 280 def test_assignable_versions
281 281 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
282 282 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
283 283 end
284 284
285 285 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
286 286 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
287 287 assert !issue.save
288 288 assert_not_nil issue.errors.on(:fixed_version_id)
289 289 end
290 290
291 291 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
292 292 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
293 293 assert !issue.save
294 294 assert_not_nil issue.errors.on(:fixed_version_id)
295 295 end
296 296
297 297 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
298 298 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
299 299 assert issue.save
300 300 end
301 301
302 302 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
303 303 issue = Issue.find(11)
304 304 assert_equal 'closed', issue.fixed_version.status
305 305 issue.subject = 'Subject changed'
306 306 assert issue.save
307 307 end
308 308
309 309 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
310 310 issue = Issue.find(11)
311 311 issue.status_id = 1
312 312 assert !issue.save
313 313 assert_not_nil issue.errors.on_base
314 314 end
315 315
316 316 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
317 317 issue = Issue.find(11)
318 318 issue.status_id = 1
319 319 issue.fixed_version_id = 3
320 320 assert issue.save
321 321 end
322 322
323 323 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
324 324 issue = Issue.find(12)
325 325 assert_equal 'locked', issue.fixed_version.status
326 326 issue.status_id = 1
327 327 assert issue.save
328 328 end
329 329
330 330 def test_move_to_another_project_with_same_category
331 331 issue = Issue.find(1)
332 332 assert issue.move_to(Project.find(2))
333 333 issue.reload
334 334 assert_equal 2, issue.project_id
335 335 # Category changes
336 336 assert_equal 4, issue.category_id
337 337 # Make sure time entries were move to the target project
338 338 assert_equal 2, issue.time_entries.first.project_id
339 339 end
340 340
341 341 def test_move_to_another_project_without_same_category
342 342 issue = Issue.find(2)
343 343 assert issue.move_to(Project.find(2))
344 344 issue.reload
345 345 assert_equal 2, issue.project_id
346 346 # Category cleared
347 347 assert_nil issue.category_id
348 348 end
349 349
350 350 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
351 351 issue = Issue.find(1)
352 352 issue.update_attribute(:fixed_version_id, 1)
353 353 assert issue.move_to(Project.find(2))
354 354 issue.reload
355 355 assert_equal 2, issue.project_id
356 356 # Cleared fixed_version
357 357 assert_equal nil, issue.fixed_version
358 358 end
359 359
360 360 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
361 361 issue = Issue.find(1)
362 362 issue.update_attribute(:fixed_version_id, 4)
363 363 assert issue.move_to(Project.find(5))
364 364 issue.reload
365 365 assert_equal 5, issue.project_id
366 366 # Keep fixed_version
367 367 assert_equal 4, issue.fixed_version_id
368 368 end
369 369
370 370 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
371 371 issue = Issue.find(1)
372 372 issue.update_attribute(:fixed_version_id, 1)
373 373 assert issue.move_to(Project.find(5))
374 374 issue.reload
375 375 assert_equal 5, issue.project_id
376 376 # Cleared fixed_version
377 377 assert_equal nil, issue.fixed_version
378 378 end
379 379
380 380 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
381 381 issue = Issue.find(1)
382 382 issue.update_attribute(:fixed_version_id, 7)
383 383 assert issue.move_to(Project.find(2))
384 384 issue.reload
385 385 assert_equal 2, issue.project_id
386 386 # Keep fixed_version
387 387 assert_equal 7, issue.fixed_version_id
388 388 end
389 389
390 390 def test_copy_to_the_same_project
391 391 issue = Issue.find(1)
392 392 copy = nil
393 393 assert_difference 'Issue.count' do
394 394 copy = issue.move_to(issue.project, nil, :copy => true)
395 395 end
396 396 assert_kind_of Issue, copy
397 397 assert_equal issue.project, copy.project
398 398 assert_equal "125", copy.custom_value_for(2).value
399 399 end
400 400
401 401 def test_copy_to_another_project_and_tracker
402 402 issue = Issue.find(1)
403 403 copy = nil
404 404 assert_difference 'Issue.count' do
405 405 copy = issue.move_to(Project.find(3), Tracker.find(2), :copy => true)
406 406 end
407 407 assert_kind_of Issue, copy
408 408 assert_equal Project.find(3), copy.project
409 409 assert_equal Tracker.find(2), copy.tracker
410 410 # Custom field #2 is not associated with target tracker
411 411 assert_nil copy.custom_value_for(2)
412 412 end
413 413
414 414 context "#move_to" do
415 415 context "as a copy" do
416 416 setup do
417 417 @issue = Issue.find(1)
418 418 @copy = nil
419 419 end
420 420
421 421 should "allow assigned_to changes" do
422 422 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
423 423 assert_equal 3, @copy.assigned_to_id
424 424 end
425 425
426 426 should "allow status changes" do
427 427 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
428 428 assert_equal 2, @copy.status_id
429 429 end
430 430
431 431 should "allow start date changes" do
432 432 date = Date.today
433 433 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
434 434 assert_equal date, @copy.start_date
435 435 end
436 436
437 437 should "allow due date changes" do
438 438 date = Date.today
439 439 @copy = @issue.move_to(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
440 440
441 441 assert_equal date, @copy.due_date
442 442 end
443 443 end
444 444 end
445 445
446 446 def test_recipients_should_not_include_users_that_cannot_view_the_issue
447 447 issue = Issue.find(12)
448 448 assert issue.recipients.include?(issue.author.mail)
449 449 # move the issue to a private project
450 450 copy = issue.move_to(Project.find(5), Tracker.find(2), :copy => true)
451 451 # author is not a member of project anymore
452 452 assert !copy.recipients.include?(copy.author.mail)
453 453 end
454 454
455 455 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
456 456 user = User.find(3)
457 457 issue = Issue.find(9)
458 458 Watcher.create!(:user => user, :watchable => issue)
459 459 assert issue.watched_by?(user)
460 460 assert !issue.watcher_recipients.include?(user.mail)
461 461 end
462 462
463 463 def test_issue_destroy
464 464 Issue.find(1).destroy
465 465 assert_nil Issue.find_by_id(1)
466 466 assert_nil TimeEntry.find_by_issue_id(1)
467 467 end
468 468
469 469 def test_blocked
470 470 blocked_issue = Issue.find(9)
471 471 blocking_issue = Issue.find(10)
472 472
473 473 assert blocked_issue.blocked?
474 474 assert !blocking_issue.blocked?
475 475 end
476 476
477 477 def test_blocked_issues_dont_allow_closed_statuses
478 478 blocked_issue = Issue.find(9)
479 479
480 480 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
481 481 assert !allowed_statuses.empty?
482 482 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
483 483 assert closed_statuses.empty?
484 484 end
485 485
486 486 def test_unblocked_issues_allow_closed_statuses
487 487 blocking_issue = Issue.find(10)
488 488
489 489 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
490 490 assert !allowed_statuses.empty?
491 491 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
492 492 assert !closed_statuses.empty?
493 493 end
494 494
495 495 def test_overdue
496 496 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
497 497 assert !Issue.new(:due_date => Date.today).overdue?
498 498 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
499 499 assert !Issue.new(:due_date => nil).overdue?
500 500 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
501 501 end
502 502
503 503 def test_assignable_users
504 504 assert_kind_of User, Issue.find(1).assignable_users.first
505 505 end
506 506
507 507 def test_create_should_send_email_notification
508 508 ActionMailer::Base.deliveries.clear
509 509 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')
510 510
511 511 assert issue.save
512 512 assert_equal 1, ActionMailer::Base.deliveries.size
513 513 end
514 514
515 515 def test_stale_issue_should_not_send_email_notification
516 516 ActionMailer::Base.deliveries.clear
517 517 issue = Issue.find(1)
518 518 stale = Issue.find(1)
519 519
520 520 issue.init_journal(User.find(1))
521 521 issue.subject = 'Subjet update'
522 522 assert issue.save
523 523 assert_equal 1, ActionMailer::Base.deliveries.size
524 524 ActionMailer::Base.deliveries.clear
525 525
526 526 stale.init_journal(User.find(1))
527 527 stale.subject = 'Another subjet update'
528 528 assert_raise ActiveRecord::StaleObjectError do
529 529 stale.save
530 530 end
531 531 assert ActionMailer::Base.deliveries.empty?
532 532 end
533 533
534 534 def test_saving_twice_should_not_duplicate_journal_details
535 535 i = Issue.find(:first)
536 536 i.init_journal(User.find(2), 'Some notes')
537 537 # initial changes
538 538 i.subject = 'New subject'
539 539 i.done_ratio = i.done_ratio + 10
540 540 assert_difference 'Journal.count' do
541 541 assert i.save
542 542 end
543 543 # 1 more change
544 544 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
545 545 assert_no_difference 'Journal.count' do
546 546 assert_difference 'JournalDetail.count', 1 do
547 547 i.save
548 548 end
549 549 end
550 550 # no more change
551 551 assert_no_difference 'Journal.count' do
552 552 assert_no_difference 'JournalDetail.count' do
553 553 i.save
554 554 end
555 555 end
556 556 end
557 557
558 558 context "#done_ratio" do
559 559 setup do
560 560 @issue = Issue.find(1)
561 561 @issue_status = IssueStatus.find(1)
562 562 @issue_status.update_attribute(:default_done_ratio, 50)
563 563 end
564 564
565 565 context "with Setting.issue_done_ratio using the issue_field" do
566 566 setup do
567 567 Setting.issue_done_ratio = 'issue_field'
568 568 end
569 569
570 570 should "read the issue's field" do
571 571 assert_equal 0, @issue.done_ratio
572 572 end
573 573 end
574 574
575 575 context "with Setting.issue_done_ratio using the issue_status" do
576 576 setup do
577 577 Setting.issue_done_ratio = 'issue_status'
578 578 end
579 579
580 580 should "read the Issue Status's default done ratio" do
581 581 assert_equal 50, @issue.done_ratio
582 582 end
583 583 end
584 584 end
585 585
586 586 context "#update_done_ratio_from_issue_status" do
587 587 setup do
588 588 @issue = Issue.find(1)
589 589 @issue_status = IssueStatus.find(1)
590 590 @issue_status.update_attribute(:default_done_ratio, 50)
591 591 end
592 592
593 593 context "with Setting.issue_done_ratio using the issue_field" do
594 594 setup do
595 595 Setting.issue_done_ratio = 'issue_field'
596 596 end
597 597
598 598 should "not change the issue" do
599 599 @issue.update_done_ratio_from_issue_status
600 600
601 601 assert_equal 0, @issue.done_ratio
602 602 end
603 603 end
604 604
605 605 context "with Setting.issue_done_ratio using the issue_status" do
606 606 setup do
607 607 Setting.issue_done_ratio = 'issue_status'
608 608 end
609 609
610 610 should "not change the issue's done ratio" do
611 611 @issue.update_done_ratio_from_issue_status
612 612
613 613 assert_equal 50, @issue.done_ratio
614 614 end
615 615 end
616 616 end
617 617
618 618 test "#by_tracker" do
619 619 groups = Issue.by_tracker(Project.find(1))
620 620 assert_equal 3, groups.size
621 621 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
622 622 end
623 623
624 624 test "#by_version" do
625 625 groups = Issue.by_version(Project.find(1))
626 626 assert_equal 3, groups.size
627 627 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
628 628 end
629 629
630 630 test "#by_priority" do
631 631 groups = Issue.by_priority(Project.find(1))
632 632 assert_equal 4, groups.size
633 633 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
634 634 end
635 635
636 636 test "#by_category" do
637 637 groups = Issue.by_category(Project.find(1))
638 638 assert_equal 2, groups.size
639 639 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
640 640 end
641 641
642 642 test "#by_assigned_to" do
643 643 groups = Issue.by_assigned_to(Project.find(1))
644 644 assert_equal 2, groups.size
645 645 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
646 646 end
647 647
648 648 test "#by_author" do
649 649 groups = Issue.by_author(Project.find(1))
650 650 assert_equal 4, groups.size
651 651 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
652 652 end
653 653
654 654 test "#by_subproject" do
655 655 groups = Issue.by_subproject(Project.find(1))
656 656 assert_equal 2, groups.size
657 657 assert_equal 5, groups.inject(0) {|sum, group| sum + group['total'].to_i}
658 658 end
659
660 def test_recently_updated_with_limit_scopes
661 #should return the last updated issue
662 assert_equal 1, Issue.recently_updated.with_limit(1).length
663 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
664 end
665
666 def test_on_active_projects_scope
667 assert Project.find(2).archive
668
669 before = Issue.on_active_project.length
670 # test inclusion to results
671 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
672 assert_equal before + 1, Issue.on_active_project.length
673
674 # Move to an archived project
675 issue.project = Project.find(2)
676 assert issue.save
677 assert_equal before, Issue.on_active_project.length
678 end
659 679 end
@@ -1,74 +1,71
1 1 # ActsAsWatchable
2 2 module Redmine
3 3 module Acts
4 4 module Watchable
5 5 def self.included(base)
6 6 base.extend ClassMethods
7 7 end
8 8
9 9 module ClassMethods
10 10 def acts_as_watchable(options = {})
11 11 return if self.included_modules.include?(Redmine::Acts::Watchable::InstanceMethods)
12 12 send :include, Redmine::Acts::Watchable::InstanceMethods
13 13
14 14 class_eval do
15 15 has_many :watchers, :as => :watchable, :dependent => :delete_all
16 16 has_many :watcher_users, :through => :watchers, :source => :user
17 17
18 named_scope :watched_by, lambda { |user_id|
19 { :include => :watchers,
20 :conditions => ["#{Watcher.table_name}.user_id = ?", user_id] }
21 }
18 22 attr_protected :watcher_ids, :watcher_user_ids
19 23 end
20 24 end
21 25 end
22 26
23 27 module InstanceMethods
24 28 def self.included(base)
25 29 base.extend ClassMethods
26 30 end
27 31
28 32 # Returns an array of users that are proposed as watchers
29 33 def addable_watcher_users
30 34 self.project.users.sort - self.watcher_users
31 35 end
32 36
33 37 # Adds user as a watcher
34 38 def add_watcher(user)
35 39 self.watchers << Watcher.new(:user => user)
36 40 end
37 41
38 42 # Removes user from the watchers list
39 43 def remove_watcher(user)
40 44 return nil unless user && user.is_a?(User)
41 45 Watcher.delete_all "watchable_type = '#{self.class}' AND watchable_id = #{self.id} AND user_id = #{user.id}"
42 46 end
43 47
44 48 # Adds/removes watcher
45 49 def set_watcher(user, watching=true)
46 50 watching ? add_watcher(user) : remove_watcher(user)
47 51 end
48 52
49 53 # Returns true if object is watched by user
50 54 def watched_by?(user)
51 55 !!(user && self.watchers.detect {|w| w.user_id == user.id })
52 56 end
53 57
54 58 # Returns an array of watchers' email addresses
55 59 def watcher_recipients
56 60 notified = watchers.collect(&:user).select(&:active?)
57 61 if respond_to?(:visible?)
58 62 notified.reject! {|user| !visible?(user)}
59 63 end
60 64 notified.collect(&:mail).compact
61 65 end
62 66
63 module ClassMethods
64 # Returns the objects that are watched by user
65 def watched_by(user)
66 find(:all,
67 :include => :watchers,
68 :conditions => ["#{Watcher.table_name}.user_id = ?", user.id])
69 end
70 end
67 module ClassMethods; end
71 68 end
72 69 end
73 70 end
74 71 end
General Comments 0
You need to be logged in to leave comments. Login now