##// END OF EJS Templates
Merged r13124 (#16755)....
Jean-Philippe Lang -
r12886:cc37898d15bd
parent child
Show More
@@ -1,84 +1,84
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 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 ContextMenusController < ApplicationController
18 class ContextMenusController < ApplicationController
19 helper :watchers
19 helper :watchers
20 helper :issues
20 helper :issues
21
21
22 before_filter :find_issues, :only => :issues
22 before_filter :find_issues, :only => :issues
23
23
24 def issues
24 def issues
25 if (@issues.size == 1)
25 if (@issues.size == 1)
26 @issue = @issues.first
26 @issue = @issues.first
27 end
27 end
28 @issue_ids = @issues.map(&:id).sort
28 @issue_ids = @issues.map(&:id).sort
29
29
30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
30 @allowed_statuses = @issues.map(&:new_statuses_allowed_to).reduce(:&)
31
31
32 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
32 @can = {:edit => User.current.allowed_to?(:edit_issues, @projects),
33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
33 :log_time => (@project && User.current.allowed_to?(:log_time, @project)),
34 :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
34 :update => (User.current.allowed_to?(:edit_issues, @projects) || (User.current.allowed_to?(:change_status, @projects) && !@allowed_statuses.blank?)),
35 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
35 :move => (@project && User.current.allowed_to?(:move_issues, @project)),
36 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
36 :copy => (@issue && @project.trackers.include?(@issue.tracker) && User.current.allowed_to?(:add_issues, @project)),
37 :delete => User.current.allowed_to?(:delete_issues, @projects)
37 :delete => User.current.allowed_to?(:delete_issues, @projects)
38 }
38 }
39 if @project
39 if @project
40 if @issue
40 if @issue
41 @assignables = @issue.assignable_users
41 @assignables = @issue.assignable_users
42 else
42 else
43 @assignables = @project.assignable_users
43 @assignables = @project.assignable_users
44 end
44 end
45 @trackers = @project.trackers
45 @trackers = @project.trackers
46 else
46 else
47 #when multiple projects, we only keep the intersection of each set
47 #when multiple projects, we only keep the intersection of each set
48 @assignables = @projects.map(&:assignable_users).reduce(:&)
48 @assignables = @projects.map(&:assignable_users).reduce(:&)
49 @trackers = @projects.map(&:trackers).reduce(:&)
49 @trackers = @projects.map(&:trackers).reduce(:&)
50 end
50 end
51 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
51 @versions = @projects.map {|p| p.shared_versions.open}.reduce(:&)
52
52
53 @priorities = IssuePriority.active.reverse
53 @priorities = IssuePriority.active.reverse
54 @back = back_url
54 @back = back_url
55
55
56 @options_by_custom_field = {}
56 @options_by_custom_field = {}
57 if @can[:edit]
57 if @can[:edit]
58 custom_fields = @issues.map(&:available_custom_fields).reduce(:&).reject(&:multiple?)
58 custom_fields = @issues.map(&:editable_custom_fields).reduce(:&).reject(&:multiple?)
59 custom_fields.each do |field|
59 custom_fields.each do |field|
60 values = field.possible_values_options(@projects)
60 values = field.possible_values_options(@projects)
61 if values.present?
61 if values.present?
62 @options_by_custom_field[field] = values
62 @options_by_custom_field[field] = values
63 end
63 end
64 end
64 end
65 end
65 end
66
66
67 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
67 @safe_attributes = @issues.map(&:safe_attribute_names).reduce(:&)
68 render :layout => false
68 render :layout => false
69 end
69 end
70
70
71 def time_entries
71 def time_entries
72 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
72 @time_entries = TimeEntry.where(:id => params[:ids]).preload(:project).to_a
73 (render_404; return) unless @time_entries.present?
73 (render_404; return) unless @time_entries.present?
74
74
75 @projects = @time_entries.collect(&:project).compact.uniq
75 @projects = @time_entries.collect(&:project).compact.uniq
76 @project = @projects.first if @projects.size == 1
76 @project = @projects.first if @projects.size == 1
77 @activities = TimeEntryActivity.shared.active
77 @activities = TimeEntryActivity.shared.active
78 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
78 @can = {:edit => User.current.allowed_to?(:edit_time_entries, @projects),
79 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
79 :delete => User.current.allowed_to?(:edit_time_entries, @projects)
80 }
80 }
81 @back = back_url
81 @back = back_url
82 render :layout => false
82 render :layout => false
83 end
83 end
84 end
84 end
@@ -1,1566 +1,1571
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21 include Redmine::I18n
21 include Redmine::I18n
22
22
23 belongs_to :project
23 belongs_to :project
24 belongs_to :tracker
24 belongs_to :tracker
25 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
25 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
26 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
26 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
27 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
27 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
28 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
28 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
29 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
29 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
30 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
30 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
31
31
32 has_many :journals, :as => :journalized, :dependent => :destroy
32 has_many :journals, :as => :journalized, :dependent => :destroy
33 has_many :visible_journals,
33 has_many :visible_journals,
34 :class_name => 'Journal',
34 :class_name => 'Journal',
35 :as => :journalized,
35 :as => :journalized,
36 :conditions => Proc.new {
36 :conditions => Proc.new {
37 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
37 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
38 },
38 },
39 :readonly => true
39 :readonly => true
40
40
41 has_many :time_entries, :dependent => :destroy
41 has_many :time_entries, :dependent => :destroy
42 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
42 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
43
43
44 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
45 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
46
46
47 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
47 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
48 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
48 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
49 acts_as_customizable
49 acts_as_customizable
50 acts_as_watchable
50 acts_as_watchable
51 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
51 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
52 :include => [:project, :visible_journals],
52 :include => [:project, :visible_journals],
53 # sort by id so that limited eager loading doesn't break with postgresql
53 # sort by id so that limited eager loading doesn't break with postgresql
54 :order_column => "#{table_name}.id"
54 :order_column => "#{table_name}.id"
55 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
55 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
56 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
56 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
57 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
57 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
58
58
59 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
59 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
60 :author_key => :author_id
60 :author_key => :author_id
61
61
62 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
62 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
63
63
64 attr_reader :current_journal
64 attr_reader :current_journal
65 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
65 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
66
66
67 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
67 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
68
68
69 validates_length_of :subject, :maximum => 255
69 validates_length_of :subject, :maximum => 255
70 validates_inclusion_of :done_ratio, :in => 0..100
70 validates_inclusion_of :done_ratio, :in => 0..100
71 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
71 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
72 validates :start_date, :date => true
72 validates :start_date, :date => true
73 validates :due_date, :date => true
73 validates :due_date, :date => true
74 validate :validate_issue, :validate_required_fields
74 validate :validate_issue, :validate_required_fields
75
75
76 scope :visible, lambda {|*args|
76 scope :visible, lambda {|*args|
77 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
77 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
78 }
78 }
79
79
80 scope :open, lambda {|*args|
80 scope :open, lambda {|*args|
81 is_closed = args.size > 0 ? !args.first : false
81 is_closed = args.size > 0 ? !args.first : false
82 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
82 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
83 }
83 }
84
84
85 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
86 scope :on_active_project, lambda {
86 scope :on_active_project, lambda {
87 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
87 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
88 }
88 }
89 scope :fixed_version, lambda {|versions|
89 scope :fixed_version, lambda {|versions|
90 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
90 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
91 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
91 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
92 }
92 }
93
93
94 before_create :default_assign
94 before_create :default_assign
95 before_save :close_duplicates, :update_done_ratio_from_issue_status,
95 before_save :close_duplicates, :update_done_ratio_from_issue_status,
96 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
96 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
97 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
97 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
98 after_save :reschedule_following_issues, :update_nested_set_attributes,
98 after_save :reschedule_following_issues, :update_nested_set_attributes,
99 :update_parent_attributes, :create_journal
99 :update_parent_attributes, :create_journal
100 # Should be after_create but would be called before previous after_save callbacks
100 # Should be after_create but would be called before previous after_save callbacks
101 after_save :after_create_from_copy
101 after_save :after_create_from_copy
102 after_destroy :update_parent_attributes
102 after_destroy :update_parent_attributes
103 after_create :send_notification
103 after_create :send_notification
104 # Keep it at the end of after_save callbacks
104 # Keep it at the end of after_save callbacks
105 after_save :clear_assigned_to_was
105 after_save :clear_assigned_to_was
106
106
107 # Returns a SQL conditions string used to find all issues visible by the specified user
107 # Returns a SQL conditions string used to find all issues visible by the specified user
108 def self.visible_condition(user, options={})
108 def self.visible_condition(user, options={})
109 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
109 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
110 if user.logged?
110 if user.logged?
111 case role.issues_visibility
111 case role.issues_visibility
112 when 'all'
112 when 'all'
113 nil
113 nil
114 when 'default'
114 when 'default'
115 user_ids = [user.id] + user.groups.map(&:id).compact
115 user_ids = [user.id] + user.groups.map(&:id).compact
116 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
116 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
117 when 'own'
117 when 'own'
118 user_ids = [user.id] + user.groups.map(&:id).compact
118 user_ids = [user.id] + user.groups.map(&:id).compact
119 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
119 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
120 else
120 else
121 '1=0'
121 '1=0'
122 end
122 end
123 else
123 else
124 "(#{table_name}.is_private = #{connection.quoted_false})"
124 "(#{table_name}.is_private = #{connection.quoted_false})"
125 end
125 end
126 end
126 end
127 end
127 end
128
128
129 # Returns true if usr or current user is allowed to view the issue
129 # Returns true if usr or current user is allowed to view the issue
130 def visible?(usr=nil)
130 def visible?(usr=nil)
131 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
131 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
132 if user.logged?
132 if user.logged?
133 case role.issues_visibility
133 case role.issues_visibility
134 when 'all'
134 when 'all'
135 true
135 true
136 when 'default'
136 when 'default'
137 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
137 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
138 when 'own'
138 when 'own'
139 self.author == user || user.is_or_belongs_to?(assigned_to)
139 self.author == user || user.is_or_belongs_to?(assigned_to)
140 else
140 else
141 false
141 false
142 end
142 end
143 else
143 else
144 !self.is_private?
144 !self.is_private?
145 end
145 end
146 end
146 end
147 end
147 end
148
148
149 # Returns true if user or current user is allowed to edit or add a note to the issue
149 # Returns true if user or current user is allowed to edit or add a note to the issue
150 def editable?(user=User.current)
150 def editable?(user=User.current)
151 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
151 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
152 end
152 end
153
153
154 def initialize(attributes=nil, *args)
154 def initialize(attributes=nil, *args)
155 super
155 super
156 if new_record?
156 if new_record?
157 # set default values for new records only
157 # set default values for new records only
158 self.status ||= IssueStatus.default
158 self.status ||= IssueStatus.default
159 self.priority ||= IssuePriority.default
159 self.priority ||= IssuePriority.default
160 self.watcher_user_ids = []
160 self.watcher_user_ids = []
161 end
161 end
162 end
162 end
163
163
164 def create_or_update
164 def create_or_update
165 super
165 super
166 ensure
166 ensure
167 @status_was = nil
167 @status_was = nil
168 end
168 end
169 private :create_or_update
169 private :create_or_update
170
170
171 # AR#Persistence#destroy would raise and RecordNotFound exception
171 # AR#Persistence#destroy would raise and RecordNotFound exception
172 # if the issue was already deleted or updated (non matching lock_version).
172 # if the issue was already deleted or updated (non matching lock_version).
173 # This is a problem when bulk deleting issues or deleting a project
173 # This is a problem when bulk deleting issues or deleting a project
174 # (because an issue may already be deleted if its parent was deleted
174 # (because an issue may already be deleted if its parent was deleted
175 # first).
175 # first).
176 # The issue is reloaded by the nested_set before being deleted so
176 # The issue is reloaded by the nested_set before being deleted so
177 # the lock_version condition should not be an issue but we handle it.
177 # the lock_version condition should not be an issue but we handle it.
178 def destroy
178 def destroy
179 super
179 super
180 rescue ActiveRecord::RecordNotFound
180 rescue ActiveRecord::RecordNotFound
181 # Stale or already deleted
181 # Stale or already deleted
182 begin
182 begin
183 reload
183 reload
184 rescue ActiveRecord::RecordNotFound
184 rescue ActiveRecord::RecordNotFound
185 # The issue was actually already deleted
185 # The issue was actually already deleted
186 @destroyed = true
186 @destroyed = true
187 return freeze
187 return freeze
188 end
188 end
189 # The issue was stale, retry to destroy
189 # The issue was stale, retry to destroy
190 super
190 super
191 end
191 end
192
192
193 alias :base_reload :reload
193 alias :base_reload :reload
194 def reload(*args)
194 def reload(*args)
195 @workflow_rule_by_attribute = nil
195 @workflow_rule_by_attribute = nil
196 @assignable_versions = nil
196 @assignable_versions = nil
197 @relations = nil
197 @relations = nil
198 base_reload(*args)
198 base_reload(*args)
199 end
199 end
200
200
201 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
201 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
202 def available_custom_fields
202 def available_custom_fields
203 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
203 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
204 end
204 end
205
205
206 def visible_custom_field_values(user=nil)
206 def visible_custom_field_values(user=nil)
207 user_real = user || User.current
207 user_real = user || User.current
208 custom_field_values.select do |value|
208 custom_field_values.select do |value|
209 value.custom_field.visible_by?(project, user_real)
209 value.custom_field.visible_by?(project, user_real)
210 end
210 end
211 end
211 end
212
212
213 # Copies attributes from another issue, arg can be an id or an Issue
213 # Copies attributes from another issue, arg can be an id or an Issue
214 def copy_from(arg, options={})
214 def copy_from(arg, options={})
215 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
215 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
216 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
216 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
217 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
217 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
218 self.status = issue.status
218 self.status = issue.status
219 self.author = User.current
219 self.author = User.current
220 unless options[:attachments] == false
220 unless options[:attachments] == false
221 self.attachments = issue.attachments.map do |attachement|
221 self.attachments = issue.attachments.map do |attachement|
222 attachement.copy(:container => self)
222 attachement.copy(:container => self)
223 end
223 end
224 end
224 end
225 @copied_from = issue
225 @copied_from = issue
226 @copy_options = options
226 @copy_options = options
227 self
227 self
228 end
228 end
229
229
230 # Returns an unsaved copy of the issue
230 # Returns an unsaved copy of the issue
231 def copy(attributes=nil, copy_options={})
231 def copy(attributes=nil, copy_options={})
232 copy = self.class.new.copy_from(self, copy_options)
232 copy = self.class.new.copy_from(self, copy_options)
233 copy.attributes = attributes if attributes
233 copy.attributes = attributes if attributes
234 copy
234 copy
235 end
235 end
236
236
237 # Returns true if the issue is a copy
237 # Returns true if the issue is a copy
238 def copy?
238 def copy?
239 @copied_from.present?
239 @copied_from.present?
240 end
240 end
241
241
242 # Moves/copies an issue to a new project and tracker
242 # Moves/copies an issue to a new project and tracker
243 # Returns the moved/copied issue on success, false on failure
243 # Returns the moved/copied issue on success, false on failure
244 def move_to_project(new_project, new_tracker=nil, options={})
244 def move_to_project(new_project, new_tracker=nil, options={})
245 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
245 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
246
246
247 if options[:copy]
247 if options[:copy]
248 issue = self.copy
248 issue = self.copy
249 else
249 else
250 issue = self
250 issue = self
251 end
251 end
252
252
253 issue.init_journal(User.current, options[:notes])
253 issue.init_journal(User.current, options[:notes])
254
254
255 # Preserve previous behaviour
255 # Preserve previous behaviour
256 # #move_to_project doesn't change tracker automatically
256 # #move_to_project doesn't change tracker automatically
257 issue.send :project=, new_project, true
257 issue.send :project=, new_project, true
258 if new_tracker
258 if new_tracker
259 issue.tracker = new_tracker
259 issue.tracker = new_tracker
260 end
260 end
261 # Allow bulk setting of attributes on the issue
261 # Allow bulk setting of attributes on the issue
262 if options[:attributes]
262 if options[:attributes]
263 issue.attributes = options[:attributes]
263 issue.attributes = options[:attributes]
264 end
264 end
265
265
266 issue.save ? issue : false
266 issue.save ? issue : false
267 end
267 end
268
268
269 def status_id=(sid)
269 def status_id=(sid)
270 self.status = nil
270 self.status = nil
271 result = write_attribute(:status_id, sid)
271 result = write_attribute(:status_id, sid)
272 @workflow_rule_by_attribute = nil
272 @workflow_rule_by_attribute = nil
273 result
273 result
274 end
274 end
275
275
276 def priority_id=(pid)
276 def priority_id=(pid)
277 self.priority = nil
277 self.priority = nil
278 write_attribute(:priority_id, pid)
278 write_attribute(:priority_id, pid)
279 end
279 end
280
280
281 def category_id=(cid)
281 def category_id=(cid)
282 self.category = nil
282 self.category = nil
283 write_attribute(:category_id, cid)
283 write_attribute(:category_id, cid)
284 end
284 end
285
285
286 def fixed_version_id=(vid)
286 def fixed_version_id=(vid)
287 self.fixed_version = nil
287 self.fixed_version = nil
288 write_attribute(:fixed_version_id, vid)
288 write_attribute(:fixed_version_id, vid)
289 end
289 end
290
290
291 def tracker_id=(tid)
291 def tracker_id=(tid)
292 self.tracker = nil
292 self.tracker = nil
293 result = write_attribute(:tracker_id, tid)
293 result = write_attribute(:tracker_id, tid)
294 @custom_field_values = nil
294 @custom_field_values = nil
295 @workflow_rule_by_attribute = nil
295 @workflow_rule_by_attribute = nil
296 result
296 result
297 end
297 end
298
298
299 def project_id=(project_id)
299 def project_id=(project_id)
300 if project_id.to_s != self.project_id.to_s
300 if project_id.to_s != self.project_id.to_s
301 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
301 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
302 end
302 end
303 end
303 end
304
304
305 def project=(project, keep_tracker=false)
305 def project=(project, keep_tracker=false)
306 project_was = self.project
306 project_was = self.project
307 write_attribute(:project_id, project ? project.id : nil)
307 write_attribute(:project_id, project ? project.id : nil)
308 association_instance_set('project', project)
308 association_instance_set('project', project)
309 if project_was && project && project_was != project
309 if project_was && project && project_was != project
310 @assignable_versions = nil
310 @assignable_versions = nil
311
311
312 unless keep_tracker || project.trackers.include?(tracker)
312 unless keep_tracker || project.trackers.include?(tracker)
313 self.tracker = project.trackers.first
313 self.tracker = project.trackers.first
314 end
314 end
315 # Reassign to the category with same name if any
315 # Reassign to the category with same name if any
316 if category
316 if category
317 self.category = project.issue_categories.find_by_name(category.name)
317 self.category = project.issue_categories.find_by_name(category.name)
318 end
318 end
319 # Keep the fixed_version if it's still valid in the new_project
319 # Keep the fixed_version if it's still valid in the new_project
320 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
320 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
321 self.fixed_version = nil
321 self.fixed_version = nil
322 end
322 end
323 # Clear the parent task if it's no longer valid
323 # Clear the parent task if it's no longer valid
324 unless valid_parent_project?
324 unless valid_parent_project?
325 self.parent_issue_id = nil
325 self.parent_issue_id = nil
326 end
326 end
327 @custom_field_values = nil
327 @custom_field_values = nil
328 end
328 end
329 end
329 end
330
330
331 def description=(arg)
331 def description=(arg)
332 if arg.is_a?(String)
332 if arg.is_a?(String)
333 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
333 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
334 end
334 end
335 write_attribute(:description, arg)
335 write_attribute(:description, arg)
336 end
336 end
337
337
338 # Overrides assign_attributes so that project and tracker get assigned first
338 # Overrides assign_attributes so that project and tracker get assigned first
339 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
339 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
340 return if new_attributes.nil?
340 return if new_attributes.nil?
341 attrs = new_attributes.dup
341 attrs = new_attributes.dup
342 attrs.stringify_keys!
342 attrs.stringify_keys!
343
343
344 %w(project project_id tracker tracker_id).each do |attr|
344 %w(project project_id tracker tracker_id).each do |attr|
345 if attrs.has_key?(attr)
345 if attrs.has_key?(attr)
346 send "#{attr}=", attrs.delete(attr)
346 send "#{attr}=", attrs.delete(attr)
347 end
347 end
348 end
348 end
349 send :assign_attributes_without_project_and_tracker_first, attrs, *args
349 send :assign_attributes_without_project_and_tracker_first, attrs, *args
350 end
350 end
351 # Do not redefine alias chain on reload (see #4838)
351 # Do not redefine alias chain on reload (see #4838)
352 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
352 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
353
353
354 def estimated_hours=(h)
354 def estimated_hours=(h)
355 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
355 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
356 end
356 end
357
357
358 safe_attributes 'project_id',
358 safe_attributes 'project_id',
359 :if => lambda {|issue, user|
359 :if => lambda {|issue, user|
360 if issue.new_record?
360 if issue.new_record?
361 issue.copy?
361 issue.copy?
362 elsif user.allowed_to?(:move_issues, issue.project)
362 elsif user.allowed_to?(:move_issues, issue.project)
363 Issue.allowed_target_projects_on_move.count > 1
363 Issue.allowed_target_projects_on_move.count > 1
364 end
364 end
365 }
365 }
366
366
367 safe_attributes 'tracker_id',
367 safe_attributes 'tracker_id',
368 'status_id',
368 'status_id',
369 'category_id',
369 'category_id',
370 'assigned_to_id',
370 'assigned_to_id',
371 'priority_id',
371 'priority_id',
372 'fixed_version_id',
372 'fixed_version_id',
373 'subject',
373 'subject',
374 'description',
374 'description',
375 'start_date',
375 'start_date',
376 'due_date',
376 'due_date',
377 'done_ratio',
377 'done_ratio',
378 'estimated_hours',
378 'estimated_hours',
379 'custom_field_values',
379 'custom_field_values',
380 'custom_fields',
380 'custom_fields',
381 'lock_version',
381 'lock_version',
382 'notes',
382 'notes',
383 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
383 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
384
384
385 safe_attributes 'status_id',
385 safe_attributes 'status_id',
386 'assigned_to_id',
386 'assigned_to_id',
387 'fixed_version_id',
387 'fixed_version_id',
388 'done_ratio',
388 'done_ratio',
389 'lock_version',
389 'lock_version',
390 'notes',
390 'notes',
391 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
391 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
392
392
393 safe_attributes 'notes',
393 safe_attributes 'notes',
394 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
394 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
395
395
396 safe_attributes 'private_notes',
396 safe_attributes 'private_notes',
397 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
397 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
398
398
399 safe_attributes 'watcher_user_ids',
399 safe_attributes 'watcher_user_ids',
400 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
400 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
401
401
402 safe_attributes 'is_private',
402 safe_attributes 'is_private',
403 :if => lambda {|issue, user|
403 :if => lambda {|issue, user|
404 user.allowed_to?(:set_issues_private, issue.project) ||
404 user.allowed_to?(:set_issues_private, issue.project) ||
405 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
405 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
406 }
406 }
407
407
408 safe_attributes 'parent_issue_id',
408 safe_attributes 'parent_issue_id',
409 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
409 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
410 user.allowed_to?(:manage_subtasks, issue.project)}
410 user.allowed_to?(:manage_subtasks, issue.project)}
411
411
412 def safe_attribute_names(user=nil)
412 def safe_attribute_names(user=nil)
413 names = super
413 names = super
414 names -= disabled_core_fields
414 names -= disabled_core_fields
415 names -= read_only_attribute_names(user)
415 names -= read_only_attribute_names(user)
416 names
416 names
417 end
417 end
418
418
419 # Safely sets attributes
419 # Safely sets attributes
420 # Should be called from controllers instead of #attributes=
420 # Should be called from controllers instead of #attributes=
421 # attr_accessible is too rough because we still want things like
421 # attr_accessible is too rough because we still want things like
422 # Issue.new(:project => foo) to work
422 # Issue.new(:project => foo) to work
423 def safe_attributes=(attrs, user=User.current)
423 def safe_attributes=(attrs, user=User.current)
424 return unless attrs.is_a?(Hash)
424 return unless attrs.is_a?(Hash)
425
425
426 attrs = attrs.dup
426 attrs = attrs.dup
427
427
428 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
428 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
429 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
429 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
430 if allowed_target_projects(user).where(:id => p.to_i).exists?
430 if allowed_target_projects(user).where(:id => p.to_i).exists?
431 self.project_id = p
431 self.project_id = p
432 end
432 end
433 end
433 end
434
434
435 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
435 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
436 self.tracker_id = t
436 self.tracker_id = t
437 end
437 end
438
438
439 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
439 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
440 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
440 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
441 self.status_id = s
441 self.status_id = s
442 end
442 end
443 end
443 end
444
444
445 attrs = delete_unsafe_attributes(attrs, user)
445 attrs = delete_unsafe_attributes(attrs, user)
446 return if attrs.empty?
446 return if attrs.empty?
447
447
448 unless leaf?
448 unless leaf?
449 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
449 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
450 end
450 end
451
451
452 if attrs['parent_issue_id'].present?
452 if attrs['parent_issue_id'].present?
453 s = attrs['parent_issue_id'].to_s
453 s = attrs['parent_issue_id'].to_s
454 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
454 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
455 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
455 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
456 end
456 end
457 end
457 end
458
458
459 if attrs['custom_field_values'].present?
459 if attrs['custom_field_values'].present?
460 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
460 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
461 # TODO: use #select when ruby1.8 support is dropped
461 # TODO: use #select when ruby1.8 support is dropped
462 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| !editable_custom_field_ids.include?(k.to_s)}
462 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| !editable_custom_field_ids.include?(k.to_s)}
463 end
463 end
464
464
465 if attrs['custom_fields'].present?
465 if attrs['custom_fields'].present?
466 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
466 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
467 # TODO: use #select when ruby1.8 support is dropped
467 # TODO: use #select when ruby1.8 support is dropped
468 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| !editable_custom_field_ids.include?(c['id'].to_s)}
468 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| !editable_custom_field_ids.include?(c['id'].to_s)}
469 end
469 end
470
470
471 # mass-assignment security bypass
471 # mass-assignment security bypass
472 assign_attributes attrs, :without_protection => true
472 assign_attributes attrs, :without_protection => true
473 end
473 end
474
474
475 def disabled_core_fields
475 def disabled_core_fields
476 tracker ? tracker.disabled_core_fields : []
476 tracker ? tracker.disabled_core_fields : []
477 end
477 end
478
478
479 # Returns the custom_field_values that can be edited by the given user
479 # Returns the custom_field_values that can be edited by the given user
480 def editable_custom_field_values(user=nil)
480 def editable_custom_field_values(user=nil)
481 visible_custom_field_values(user).reject do |value|
481 visible_custom_field_values(user).reject do |value|
482 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
482 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
483 end
483 end
484 end
484 end
485
485
486 # Returns the custom fields that can be edited by the given user
487 def editable_custom_fields(user=nil)
488 editable_custom_field_values(user).map(&:custom_field).uniq
489 end
490
486 # Returns the names of attributes that are read-only for user or the current user
491 # Returns the names of attributes that are read-only for user or the current user
487 # For users with multiple roles, the read-only fields are the intersection of
492 # For users with multiple roles, the read-only fields are the intersection of
488 # read-only fields of each role
493 # read-only fields of each role
489 # The result is an array of strings where sustom fields are represented with their ids
494 # The result is an array of strings where sustom fields are represented with their ids
490 #
495 #
491 # Examples:
496 # Examples:
492 # issue.read_only_attribute_names # => ['due_date', '2']
497 # issue.read_only_attribute_names # => ['due_date', '2']
493 # issue.read_only_attribute_names(user) # => []
498 # issue.read_only_attribute_names(user) # => []
494 def read_only_attribute_names(user=nil)
499 def read_only_attribute_names(user=nil)
495 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
500 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
496 end
501 end
497
502
498 # Returns the names of required attributes for user or the current user
503 # Returns the names of required attributes for user or the current user
499 # For users with multiple roles, the required fields are the intersection of
504 # For users with multiple roles, the required fields are the intersection of
500 # required fields of each role
505 # required fields of each role
501 # The result is an array of strings where sustom fields are represented with their ids
506 # The result is an array of strings where sustom fields are represented with their ids
502 #
507 #
503 # Examples:
508 # Examples:
504 # issue.required_attribute_names # => ['due_date', '2']
509 # issue.required_attribute_names # => ['due_date', '2']
505 # issue.required_attribute_names(user) # => []
510 # issue.required_attribute_names(user) # => []
506 def required_attribute_names(user=nil)
511 def required_attribute_names(user=nil)
507 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
512 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
508 end
513 end
509
514
510 # Returns true if the attribute is required for user
515 # Returns true if the attribute is required for user
511 def required_attribute?(name, user=nil)
516 def required_attribute?(name, user=nil)
512 required_attribute_names(user).include?(name.to_s)
517 required_attribute_names(user).include?(name.to_s)
513 end
518 end
514
519
515 # Returns a hash of the workflow rule by attribute for the given user
520 # Returns a hash of the workflow rule by attribute for the given user
516 #
521 #
517 # Examples:
522 # Examples:
518 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
523 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
519 def workflow_rule_by_attribute(user=nil)
524 def workflow_rule_by_attribute(user=nil)
520 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
525 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
521
526
522 user_real = user || User.current
527 user_real = user || User.current
523 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
528 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
524 return {} if roles.empty?
529 return {} if roles.empty?
525
530
526 result = {}
531 result = {}
527 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
532 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
528 if workflow_permissions.any?
533 if workflow_permissions.any?
529 workflow_rules = workflow_permissions.inject({}) do |h, wp|
534 workflow_rules = workflow_permissions.inject({}) do |h, wp|
530 h[wp.field_name] ||= []
535 h[wp.field_name] ||= []
531 h[wp.field_name] << wp.rule
536 h[wp.field_name] << wp.rule
532 h
537 h
533 end
538 end
534 workflow_rules.each do |attr, rules|
539 workflow_rules.each do |attr, rules|
535 next if rules.size < roles.size
540 next if rules.size < roles.size
536 uniq_rules = rules.uniq
541 uniq_rules = rules.uniq
537 if uniq_rules.size == 1
542 if uniq_rules.size == 1
538 result[attr] = uniq_rules.first
543 result[attr] = uniq_rules.first
539 else
544 else
540 result[attr] = 'required'
545 result[attr] = 'required'
541 end
546 end
542 end
547 end
543 end
548 end
544 @workflow_rule_by_attribute = result if user.nil?
549 @workflow_rule_by_attribute = result if user.nil?
545 result
550 result
546 end
551 end
547 private :workflow_rule_by_attribute
552 private :workflow_rule_by_attribute
548
553
549 def done_ratio
554 def done_ratio
550 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
555 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
551 status.default_done_ratio
556 status.default_done_ratio
552 else
557 else
553 read_attribute(:done_ratio)
558 read_attribute(:done_ratio)
554 end
559 end
555 end
560 end
556
561
557 def self.use_status_for_done_ratio?
562 def self.use_status_for_done_ratio?
558 Setting.issue_done_ratio == 'issue_status'
563 Setting.issue_done_ratio == 'issue_status'
559 end
564 end
560
565
561 def self.use_field_for_done_ratio?
566 def self.use_field_for_done_ratio?
562 Setting.issue_done_ratio == 'issue_field'
567 Setting.issue_done_ratio == 'issue_field'
563 end
568 end
564
569
565 def validate_issue
570 def validate_issue
566 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
571 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
567 errors.add :due_date, :greater_than_start_date
572 errors.add :due_date, :greater_than_start_date
568 end
573 end
569
574
570 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
575 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
571 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
576 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
572 end
577 end
573
578
574 if fixed_version
579 if fixed_version
575 if !assignable_versions.include?(fixed_version)
580 if !assignable_versions.include?(fixed_version)
576 errors.add :fixed_version_id, :inclusion
581 errors.add :fixed_version_id, :inclusion
577 elsif reopened? && fixed_version.closed?
582 elsif reopened? && fixed_version.closed?
578 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
583 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
579 end
584 end
580 end
585 end
581
586
582 # Checks that the issue can not be added/moved to a disabled tracker
587 # Checks that the issue can not be added/moved to a disabled tracker
583 if project && (tracker_id_changed? || project_id_changed?)
588 if project && (tracker_id_changed? || project_id_changed?)
584 unless project.trackers.include?(tracker)
589 unless project.trackers.include?(tracker)
585 errors.add :tracker_id, :inclusion
590 errors.add :tracker_id, :inclusion
586 end
591 end
587 end
592 end
588
593
589 # Checks parent issue assignment
594 # Checks parent issue assignment
590 if @invalid_parent_issue_id.present?
595 if @invalid_parent_issue_id.present?
591 errors.add :parent_issue_id, :invalid
596 errors.add :parent_issue_id, :invalid
592 elsif @parent_issue
597 elsif @parent_issue
593 if !valid_parent_project?(@parent_issue)
598 if !valid_parent_project?(@parent_issue)
594 errors.add :parent_issue_id, :invalid
599 errors.add :parent_issue_id, :invalid
595 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
600 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
596 errors.add :parent_issue_id, :invalid
601 errors.add :parent_issue_id, :invalid
597 elsif !new_record?
602 elsif !new_record?
598 # moving an existing issue
603 # moving an existing issue
599 if @parent_issue.root_id != root_id
604 if @parent_issue.root_id != root_id
600 # we can always move to another tree
605 # we can always move to another tree
601 elsif move_possible?(@parent_issue)
606 elsif move_possible?(@parent_issue)
602 # move accepted inside tree
607 # move accepted inside tree
603 else
608 else
604 errors.add :parent_issue_id, :invalid
609 errors.add :parent_issue_id, :invalid
605 end
610 end
606 end
611 end
607 end
612 end
608 end
613 end
609
614
610 # Validates the issue against additional workflow requirements
615 # Validates the issue against additional workflow requirements
611 def validate_required_fields
616 def validate_required_fields
612 user = new_record? ? author : current_journal.try(:user)
617 user = new_record? ? author : current_journal.try(:user)
613
618
614 required_attribute_names(user).each do |attribute|
619 required_attribute_names(user).each do |attribute|
615 if attribute =~ /^\d+$/
620 if attribute =~ /^\d+$/
616 attribute = attribute.to_i
621 attribute = attribute.to_i
617 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
622 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
618 if v && v.value.blank?
623 if v && v.value.blank?
619 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
624 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
620 end
625 end
621 else
626 else
622 if respond_to?(attribute) && send(attribute).blank?
627 if respond_to?(attribute) && send(attribute).blank?
623 errors.add attribute, :blank
628 errors.add attribute, :blank
624 end
629 end
625 end
630 end
626 end
631 end
627 end
632 end
628
633
629 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
634 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
630 # even if the user turns off the setting later
635 # even if the user turns off the setting later
631 def update_done_ratio_from_issue_status
636 def update_done_ratio_from_issue_status
632 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
637 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
633 self.done_ratio = status.default_done_ratio
638 self.done_ratio = status.default_done_ratio
634 end
639 end
635 end
640 end
636
641
637 def init_journal(user, notes = "")
642 def init_journal(user, notes = "")
638 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
643 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
639 if new_record?
644 if new_record?
640 @current_journal.notify = false
645 @current_journal.notify = false
641 else
646 else
642 @attributes_before_change = attributes.dup
647 @attributes_before_change = attributes.dup
643 @custom_values_before_change = {}
648 @custom_values_before_change = {}
644 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
649 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
645 end
650 end
646 @current_journal
651 @current_journal
647 end
652 end
648
653
649 # Returns the id of the last journal or nil
654 # Returns the id of the last journal or nil
650 def last_journal_id
655 def last_journal_id
651 if new_record?
656 if new_record?
652 nil
657 nil
653 else
658 else
654 journals.maximum(:id)
659 journals.maximum(:id)
655 end
660 end
656 end
661 end
657
662
658 # Returns a scope for journals that have an id greater than journal_id
663 # Returns a scope for journals that have an id greater than journal_id
659 def journals_after(journal_id)
664 def journals_after(journal_id)
660 scope = journals.reorder("#{Journal.table_name}.id ASC")
665 scope = journals.reorder("#{Journal.table_name}.id ASC")
661 if journal_id.present?
666 if journal_id.present?
662 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
667 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
663 end
668 end
664 scope
669 scope
665 end
670 end
666
671
667 # Returns the initial status of the issue
672 # Returns the initial status of the issue
668 # Returns nil for a new issue
673 # Returns nil for a new issue
669 def status_was
674 def status_was
670 if status_id_was && status_id_was.to_i > 0
675 if status_id_was && status_id_was.to_i > 0
671 @status_was ||= IssueStatus.find_by_id(status_id_was)
676 @status_was ||= IssueStatus.find_by_id(status_id_was)
672 end
677 end
673 end
678 end
674
679
675 # Return true if the issue is closed, otherwise false
680 # Return true if the issue is closed, otherwise false
676 def closed?
681 def closed?
677 self.status.is_closed?
682 self.status.is_closed?
678 end
683 end
679
684
680 # Return true if the issue is being reopened
685 # Return true if the issue is being reopened
681 def reopened?
686 def reopened?
682 if !new_record? && status_id_changed?
687 if !new_record? && status_id_changed?
683 status_was = IssueStatus.find_by_id(status_id_was)
688 status_was = IssueStatus.find_by_id(status_id_was)
684 status_new = IssueStatus.find_by_id(status_id)
689 status_new = IssueStatus.find_by_id(status_id)
685 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
690 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
686 return true
691 return true
687 end
692 end
688 end
693 end
689 false
694 false
690 end
695 end
691
696
692 # Return true if the issue is being closed
697 # Return true if the issue is being closed
693 def closing?
698 def closing?
694 if !new_record? && status_id_changed?
699 if !new_record? && status_id_changed?
695 if status_was && status && !status_was.is_closed? && status.is_closed?
700 if status_was && status && !status_was.is_closed? && status.is_closed?
696 return true
701 return true
697 end
702 end
698 end
703 end
699 false
704 false
700 end
705 end
701
706
702 # Returns true if the issue is overdue
707 # Returns true if the issue is overdue
703 def overdue?
708 def overdue?
704 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
709 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
705 end
710 end
706
711
707 # Is the amount of work done less than it should for the due date
712 # Is the amount of work done less than it should for the due date
708 def behind_schedule?
713 def behind_schedule?
709 return false if start_date.nil? || due_date.nil?
714 return false if start_date.nil? || due_date.nil?
710 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
715 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
711 return done_date <= Date.today
716 return done_date <= Date.today
712 end
717 end
713
718
714 # Does this issue have children?
719 # Does this issue have children?
715 def children?
720 def children?
716 !leaf?
721 !leaf?
717 end
722 end
718
723
719 # Users the issue can be assigned to
724 # Users the issue can be assigned to
720 def assignable_users
725 def assignable_users
721 users = project.assignable_users
726 users = project.assignable_users
722 users << author if author
727 users << author if author
723 users << assigned_to if assigned_to
728 users << assigned_to if assigned_to
724 users.uniq.sort
729 users.uniq.sort
725 end
730 end
726
731
727 # Versions that the issue can be assigned to
732 # Versions that the issue can be assigned to
728 def assignable_versions
733 def assignable_versions
729 return @assignable_versions if @assignable_versions
734 return @assignable_versions if @assignable_versions
730
735
731 versions = project.shared_versions.open.all
736 versions = project.shared_versions.open.all
732 if fixed_version
737 if fixed_version
733 if fixed_version_id_changed?
738 if fixed_version_id_changed?
734 # nothing to do
739 # nothing to do
735 elsif project_id_changed?
740 elsif project_id_changed?
736 if project.shared_versions.include?(fixed_version)
741 if project.shared_versions.include?(fixed_version)
737 versions << fixed_version
742 versions << fixed_version
738 end
743 end
739 else
744 else
740 versions << fixed_version
745 versions << fixed_version
741 end
746 end
742 end
747 end
743 @assignable_versions = versions.uniq.sort
748 @assignable_versions = versions.uniq.sort
744 end
749 end
745
750
746 # Returns true if this issue is blocked by another issue that is still open
751 # Returns true if this issue is blocked by another issue that is still open
747 def blocked?
752 def blocked?
748 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
753 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
749 end
754 end
750
755
751 # Returns an array of statuses that user is able to apply
756 # Returns an array of statuses that user is able to apply
752 def new_statuses_allowed_to(user=User.current, include_default=false)
757 def new_statuses_allowed_to(user=User.current, include_default=false)
753 if new_record? && @copied_from
758 if new_record? && @copied_from
754 [IssueStatus.default, @copied_from.status].compact.uniq.sort
759 [IssueStatus.default, @copied_from.status].compact.uniq.sort
755 else
760 else
756 initial_status = nil
761 initial_status = nil
757 if new_record?
762 if new_record?
758 initial_status = IssueStatus.default
763 initial_status = IssueStatus.default
759 elsif status_id_was
764 elsif status_id_was
760 initial_status = IssueStatus.find_by_id(status_id_was)
765 initial_status = IssueStatus.find_by_id(status_id_was)
761 end
766 end
762 initial_status ||= status
767 initial_status ||= status
763
768
764 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
769 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
765 assignee_transitions_allowed = initial_assigned_to_id.present? &&
770 assignee_transitions_allowed = initial_assigned_to_id.present? &&
766 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
771 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
767
772
768 statuses = initial_status.find_new_statuses_allowed_to(
773 statuses = initial_status.find_new_statuses_allowed_to(
769 user.admin ? Role.all : user.roles_for_project(project),
774 user.admin ? Role.all : user.roles_for_project(project),
770 tracker,
775 tracker,
771 author == user,
776 author == user,
772 assignee_transitions_allowed
777 assignee_transitions_allowed
773 )
778 )
774 statuses << initial_status unless statuses.empty?
779 statuses << initial_status unless statuses.empty?
775 statuses << IssueStatus.default if include_default
780 statuses << IssueStatus.default if include_default
776 statuses = statuses.compact.uniq.sort
781 statuses = statuses.compact.uniq.sort
777 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
782 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
778 end
783 end
779 end
784 end
780
785
781 # Returns the previous assignee if changed
786 # Returns the previous assignee if changed
782 def assigned_to_was
787 def assigned_to_was
783 # assigned_to_id_was is reset before after_save callbacks
788 # assigned_to_id_was is reset before after_save callbacks
784 user_id = @previous_assigned_to_id || assigned_to_id_was
789 user_id = @previous_assigned_to_id || assigned_to_id_was
785 if user_id && user_id != assigned_to_id
790 if user_id && user_id != assigned_to_id
786 @assigned_to_was ||= User.find_by_id(user_id)
791 @assigned_to_was ||= User.find_by_id(user_id)
787 end
792 end
788 end
793 end
789
794
790 # Returns the users that should be notified
795 # Returns the users that should be notified
791 def notified_users
796 def notified_users
792 notified = []
797 notified = []
793 # Author and assignee are always notified unless they have been
798 # Author and assignee are always notified unless they have been
794 # locked or don't want to be notified
799 # locked or don't want to be notified
795 notified << author if author
800 notified << author if author
796 if assigned_to
801 if assigned_to
797 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
802 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
798 end
803 end
799 if assigned_to_was
804 if assigned_to_was
800 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
805 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
801 end
806 end
802 notified = notified.select {|u| u.active? && u.notify_about?(self)}
807 notified = notified.select {|u| u.active? && u.notify_about?(self)}
803
808
804 notified += project.notified_users
809 notified += project.notified_users
805 notified.uniq!
810 notified.uniq!
806 # Remove users that can not view the issue
811 # Remove users that can not view the issue
807 notified.reject! {|user| !visible?(user)}
812 notified.reject! {|user| !visible?(user)}
808 notified
813 notified
809 end
814 end
810
815
811 # Returns the email addresses that should be notified
816 # Returns the email addresses that should be notified
812 def recipients
817 def recipients
813 notified_users.collect(&:mail)
818 notified_users.collect(&:mail)
814 end
819 end
815
820
816 def each_notification(users, &block)
821 def each_notification(users, &block)
817 if users.any?
822 if users.any?
818 if custom_field_values.detect {|value| !value.custom_field.visible?}
823 if custom_field_values.detect {|value| !value.custom_field.visible?}
819 users_by_custom_field_visibility = users.group_by do |user|
824 users_by_custom_field_visibility = users.group_by do |user|
820 visible_custom_field_values(user).map(&:custom_field_id).sort
825 visible_custom_field_values(user).map(&:custom_field_id).sort
821 end
826 end
822 users_by_custom_field_visibility.values.each do |users|
827 users_by_custom_field_visibility.values.each do |users|
823 yield(users)
828 yield(users)
824 end
829 end
825 else
830 else
826 yield(users)
831 yield(users)
827 end
832 end
828 end
833 end
829 end
834 end
830
835
831 # Returns the number of hours spent on this issue
836 # Returns the number of hours spent on this issue
832 def spent_hours
837 def spent_hours
833 @spent_hours ||= time_entries.sum(:hours) || 0
838 @spent_hours ||= time_entries.sum(:hours) || 0
834 end
839 end
835
840
836 # Returns the total number of hours spent on this issue and its descendants
841 # Returns the total number of hours spent on this issue and its descendants
837 #
842 #
838 # Example:
843 # Example:
839 # spent_hours => 0.0
844 # spent_hours => 0.0
840 # spent_hours => 50.2
845 # spent_hours => 50.2
841 def total_spent_hours
846 def total_spent_hours
842 @total_spent_hours ||=
847 @total_spent_hours ||=
843 self_and_descendants.
848 self_and_descendants.
844 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
849 joins("LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").
845 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
850 sum("#{TimeEntry.table_name}.hours").to_f || 0.0
846 end
851 end
847
852
848 def relations
853 def relations
849 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
854 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
850 end
855 end
851
856
852 # Preloads relations for a collection of issues
857 # Preloads relations for a collection of issues
853 def self.load_relations(issues)
858 def self.load_relations(issues)
854 if issues.any?
859 if issues.any?
855 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
860 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
856 issues.each do |issue|
861 issues.each do |issue|
857 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
862 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
858 end
863 end
859 end
864 end
860 end
865 end
861
866
862 # Preloads visible spent time for a collection of issues
867 # Preloads visible spent time for a collection of issues
863 def self.load_visible_spent_hours(issues, user=User.current)
868 def self.load_visible_spent_hours(issues, user=User.current)
864 if issues.any?
869 if issues.any?
865 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
870 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
866 issues.each do |issue|
871 issues.each do |issue|
867 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
872 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
868 end
873 end
869 end
874 end
870 end
875 end
871
876
872 # Preloads visible relations for a collection of issues
877 # Preloads visible relations for a collection of issues
873 def self.load_visible_relations(issues, user=User.current)
878 def self.load_visible_relations(issues, user=User.current)
874 if issues.any?
879 if issues.any?
875 issue_ids = issues.map(&:id)
880 issue_ids = issues.map(&:id)
876 # Relations with issue_from in given issues and visible issue_to
881 # Relations with issue_from in given issues and visible issue_to
877 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
882 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
878 # Relations with issue_to in given issues and visible issue_from
883 # Relations with issue_to in given issues and visible issue_from
879 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
884 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
880
885
881 issues.each do |issue|
886 issues.each do |issue|
882 relations =
887 relations =
883 relations_from.select {|relation| relation.issue_from_id == issue.id} +
888 relations_from.select {|relation| relation.issue_from_id == issue.id} +
884 relations_to.select {|relation| relation.issue_to_id == issue.id}
889 relations_to.select {|relation| relation.issue_to_id == issue.id}
885
890
886 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
891 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
887 end
892 end
888 end
893 end
889 end
894 end
890
895
891 # Finds an issue relation given its id.
896 # Finds an issue relation given its id.
892 def find_relation(relation_id)
897 def find_relation(relation_id)
893 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
898 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
894 end
899 end
895
900
896 # Returns all the other issues that depend on the issue
901 # Returns all the other issues that depend on the issue
897 # The algorithm is a modified breadth first search (bfs)
902 # The algorithm is a modified breadth first search (bfs)
898 def all_dependent_issues(except=[])
903 def all_dependent_issues(except=[])
899 # The found dependencies
904 # The found dependencies
900 dependencies = []
905 dependencies = []
901
906
902 # The visited flag for every node (issue) used by the breadth first search
907 # The visited flag for every node (issue) used by the breadth first search
903 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
908 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
904
909
905 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
910 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
906 # the issue when it is processed.
911 # the issue when it is processed.
907
912
908 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
913 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
909 # but its children will not be added to the queue when it is processed.
914 # but its children will not be added to the queue when it is processed.
910
915
911 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
916 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
912 # the queue, but its children have not been added.
917 # the queue, but its children have not been added.
913
918
914 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
919 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
915 # the children still need to be processed.
920 # the children still need to be processed.
916
921
917 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
922 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
918 # added as dependent issues. It needs no further processing.
923 # added as dependent issues. It needs no further processing.
919
924
920 issue_status = Hash.new(eNOT_DISCOVERED)
925 issue_status = Hash.new(eNOT_DISCOVERED)
921
926
922 # The queue
927 # The queue
923 queue = []
928 queue = []
924
929
925 # Initialize the bfs, add start node (self) to the queue
930 # Initialize the bfs, add start node (self) to the queue
926 queue << self
931 queue << self
927 issue_status[self] = ePROCESS_ALL
932 issue_status[self] = ePROCESS_ALL
928
933
929 while (!queue.empty?) do
934 while (!queue.empty?) do
930 current_issue = queue.shift
935 current_issue = queue.shift
931 current_issue_status = issue_status[current_issue]
936 current_issue_status = issue_status[current_issue]
932 dependencies << current_issue
937 dependencies << current_issue
933
938
934 # Add parent to queue, if not already in it.
939 # Add parent to queue, if not already in it.
935 parent = current_issue.parent
940 parent = current_issue.parent
936 parent_status = issue_status[parent]
941 parent_status = issue_status[parent]
937
942
938 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
943 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
939 queue << parent
944 queue << parent
940 issue_status[parent] = ePROCESS_RELATIONS_ONLY
945 issue_status[parent] = ePROCESS_RELATIONS_ONLY
941 end
946 end
942
947
943 # Add children to queue, but only if they are not already in it and
948 # Add children to queue, but only if they are not already in it and
944 # the children of the current node need to be processed.
949 # the children of the current node need to be processed.
945 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
950 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
946 current_issue.children.each do |child|
951 current_issue.children.each do |child|
947 next if except.include?(child)
952 next if except.include?(child)
948
953
949 if (issue_status[child] == eNOT_DISCOVERED)
954 if (issue_status[child] == eNOT_DISCOVERED)
950 queue << child
955 queue << child
951 issue_status[child] = ePROCESS_ALL
956 issue_status[child] = ePROCESS_ALL
952 elsif (issue_status[child] == eRELATIONS_PROCESSED)
957 elsif (issue_status[child] == eRELATIONS_PROCESSED)
953 queue << child
958 queue << child
954 issue_status[child] = ePROCESS_CHILDREN_ONLY
959 issue_status[child] = ePROCESS_CHILDREN_ONLY
955 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
960 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
956 queue << child
961 queue << child
957 issue_status[child] = ePROCESS_ALL
962 issue_status[child] = ePROCESS_ALL
958 end
963 end
959 end
964 end
960 end
965 end
961
966
962 # Add related issues to the queue, if they are not already in it.
967 # Add related issues to the queue, if they are not already in it.
963 current_issue.relations_from.map(&:issue_to).each do |related_issue|
968 current_issue.relations_from.map(&:issue_to).each do |related_issue|
964 next if except.include?(related_issue)
969 next if except.include?(related_issue)
965
970
966 if (issue_status[related_issue] == eNOT_DISCOVERED)
971 if (issue_status[related_issue] == eNOT_DISCOVERED)
967 queue << related_issue
972 queue << related_issue
968 issue_status[related_issue] = ePROCESS_ALL
973 issue_status[related_issue] = ePROCESS_ALL
969 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
974 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
970 queue << related_issue
975 queue << related_issue
971 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
976 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
972 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
977 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
973 queue << related_issue
978 queue << related_issue
974 issue_status[related_issue] = ePROCESS_ALL
979 issue_status[related_issue] = ePROCESS_ALL
975 end
980 end
976 end
981 end
977
982
978 # Set new status for current issue
983 # Set new status for current issue
979 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
984 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
980 issue_status[current_issue] = eALL_PROCESSED
985 issue_status[current_issue] = eALL_PROCESSED
981 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
986 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
982 issue_status[current_issue] = eRELATIONS_PROCESSED
987 issue_status[current_issue] = eRELATIONS_PROCESSED
983 end
988 end
984 end # while
989 end # while
985
990
986 # Remove the issues from the "except" parameter from the result array
991 # Remove the issues from the "except" parameter from the result array
987 dependencies -= except
992 dependencies -= except
988 dependencies.delete(self)
993 dependencies.delete(self)
989
994
990 dependencies
995 dependencies
991 end
996 end
992
997
993 # Returns an array of issues that duplicate this one
998 # Returns an array of issues that duplicate this one
994 def duplicates
999 def duplicates
995 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1000 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
996 end
1001 end
997
1002
998 # Returns the due date or the target due date if any
1003 # Returns the due date or the target due date if any
999 # Used on gantt chart
1004 # Used on gantt chart
1000 def due_before
1005 def due_before
1001 due_date || (fixed_version ? fixed_version.effective_date : nil)
1006 due_date || (fixed_version ? fixed_version.effective_date : nil)
1002 end
1007 end
1003
1008
1004 # Returns the time scheduled for this issue.
1009 # Returns the time scheduled for this issue.
1005 #
1010 #
1006 # Example:
1011 # Example:
1007 # Start Date: 2/26/09, End Date: 3/04/09
1012 # Start Date: 2/26/09, End Date: 3/04/09
1008 # duration => 6
1013 # duration => 6
1009 def duration
1014 def duration
1010 (start_date && due_date) ? due_date - start_date : 0
1015 (start_date && due_date) ? due_date - start_date : 0
1011 end
1016 end
1012
1017
1013 # Returns the duration in working days
1018 # Returns the duration in working days
1014 def working_duration
1019 def working_duration
1015 (start_date && due_date) ? working_days(start_date, due_date) : 0
1020 (start_date && due_date) ? working_days(start_date, due_date) : 0
1016 end
1021 end
1017
1022
1018 def soonest_start(reload=false)
1023 def soonest_start(reload=false)
1019 @soonest_start = nil if reload
1024 @soonest_start = nil if reload
1020 @soonest_start ||= (
1025 @soonest_start ||= (
1021 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1026 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
1022 [(@parent_issue || parent).try(:soonest_start)]
1027 [(@parent_issue || parent).try(:soonest_start)]
1023 ).compact.max
1028 ).compact.max
1024 end
1029 end
1025
1030
1026 # Sets start_date on the given date or the next working day
1031 # Sets start_date on the given date or the next working day
1027 # and changes due_date to keep the same working duration.
1032 # and changes due_date to keep the same working duration.
1028 def reschedule_on(date)
1033 def reschedule_on(date)
1029 wd = working_duration
1034 wd = working_duration
1030 date = next_working_date(date)
1035 date = next_working_date(date)
1031 self.start_date = date
1036 self.start_date = date
1032 self.due_date = add_working_days(date, wd)
1037 self.due_date = add_working_days(date, wd)
1033 end
1038 end
1034
1039
1035 # Reschedules the issue on the given date or the next working day and saves the record.
1040 # Reschedules the issue on the given date or the next working day and saves the record.
1036 # If the issue is a parent task, this is done by rescheduling its subtasks.
1041 # If the issue is a parent task, this is done by rescheduling its subtasks.
1037 def reschedule_on!(date)
1042 def reschedule_on!(date)
1038 return if date.nil?
1043 return if date.nil?
1039 if leaf?
1044 if leaf?
1040 if start_date.nil? || start_date != date
1045 if start_date.nil? || start_date != date
1041 if start_date && start_date > date
1046 if start_date && start_date > date
1042 # Issue can not be moved earlier than its soonest start date
1047 # Issue can not be moved earlier than its soonest start date
1043 date = [soonest_start(true), date].compact.max
1048 date = [soonest_start(true), date].compact.max
1044 end
1049 end
1045 reschedule_on(date)
1050 reschedule_on(date)
1046 begin
1051 begin
1047 save
1052 save
1048 rescue ActiveRecord::StaleObjectError
1053 rescue ActiveRecord::StaleObjectError
1049 reload
1054 reload
1050 reschedule_on(date)
1055 reschedule_on(date)
1051 save
1056 save
1052 end
1057 end
1053 end
1058 end
1054 else
1059 else
1055 leaves.each do |leaf|
1060 leaves.each do |leaf|
1056 if leaf.start_date
1061 if leaf.start_date
1057 # Only move subtask if it starts at the same date as the parent
1062 # Only move subtask if it starts at the same date as the parent
1058 # or if it starts before the given date
1063 # or if it starts before the given date
1059 if start_date == leaf.start_date || date > leaf.start_date
1064 if start_date == leaf.start_date || date > leaf.start_date
1060 leaf.reschedule_on!(date)
1065 leaf.reschedule_on!(date)
1061 end
1066 end
1062 else
1067 else
1063 leaf.reschedule_on!(date)
1068 leaf.reschedule_on!(date)
1064 end
1069 end
1065 end
1070 end
1066 end
1071 end
1067 end
1072 end
1068
1073
1069 def <=>(issue)
1074 def <=>(issue)
1070 if issue.nil?
1075 if issue.nil?
1071 -1
1076 -1
1072 elsif root_id != issue.root_id
1077 elsif root_id != issue.root_id
1073 (root_id || 0) <=> (issue.root_id || 0)
1078 (root_id || 0) <=> (issue.root_id || 0)
1074 else
1079 else
1075 (lft || 0) <=> (issue.lft || 0)
1080 (lft || 0) <=> (issue.lft || 0)
1076 end
1081 end
1077 end
1082 end
1078
1083
1079 def to_s
1084 def to_s
1080 "#{tracker} ##{id}: #{subject}"
1085 "#{tracker} ##{id}: #{subject}"
1081 end
1086 end
1082
1087
1083 # Returns a string of css classes that apply to the issue
1088 # Returns a string of css classes that apply to the issue
1084 def css_classes(user=User.current)
1089 def css_classes(user=User.current)
1085 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1090 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1086 s << ' closed' if closed?
1091 s << ' closed' if closed?
1087 s << ' overdue' if overdue?
1092 s << ' overdue' if overdue?
1088 s << ' child' if child?
1093 s << ' child' if child?
1089 s << ' parent' unless leaf?
1094 s << ' parent' unless leaf?
1090 s << ' private' if is_private?
1095 s << ' private' if is_private?
1091 if user.logged?
1096 if user.logged?
1092 s << ' created-by-me' if author_id == user.id
1097 s << ' created-by-me' if author_id == user.id
1093 s << ' assigned-to-me' if assigned_to_id == user.id
1098 s << ' assigned-to-me' if assigned_to_id == user.id
1094 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1099 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1095 end
1100 end
1096 s
1101 s
1097 end
1102 end
1098
1103
1099 # Unassigns issues from +version+ if it's no longer shared with issue's project
1104 # Unassigns issues from +version+ if it's no longer shared with issue's project
1100 def self.update_versions_from_sharing_change(version)
1105 def self.update_versions_from_sharing_change(version)
1101 # Update issues assigned to the version
1106 # Update issues assigned to the version
1102 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1107 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1103 end
1108 end
1104
1109
1105 # Unassigns issues from versions that are no longer shared
1110 # Unassigns issues from versions that are no longer shared
1106 # after +project+ was moved
1111 # after +project+ was moved
1107 def self.update_versions_from_hierarchy_change(project)
1112 def self.update_versions_from_hierarchy_change(project)
1108 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1113 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1109 # Update issues of the moved projects and issues assigned to a version of a moved project
1114 # Update issues of the moved projects and issues assigned to a version of a moved project
1110 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1115 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1111 end
1116 end
1112
1117
1113 def parent_issue_id=(arg)
1118 def parent_issue_id=(arg)
1114 s = arg.to_s.strip.presence
1119 s = arg.to_s.strip.presence
1115 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1120 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1116 @parent_issue.id
1121 @parent_issue.id
1117 @invalid_parent_issue_id = nil
1122 @invalid_parent_issue_id = nil
1118 elsif s.blank?
1123 elsif s.blank?
1119 @parent_issue = nil
1124 @parent_issue = nil
1120 @invalid_parent_issue_id = nil
1125 @invalid_parent_issue_id = nil
1121 else
1126 else
1122 @parent_issue = nil
1127 @parent_issue = nil
1123 @invalid_parent_issue_id = arg
1128 @invalid_parent_issue_id = arg
1124 end
1129 end
1125 end
1130 end
1126
1131
1127 def parent_issue_id
1132 def parent_issue_id
1128 if @invalid_parent_issue_id
1133 if @invalid_parent_issue_id
1129 @invalid_parent_issue_id
1134 @invalid_parent_issue_id
1130 elsif instance_variable_defined? :@parent_issue
1135 elsif instance_variable_defined? :@parent_issue
1131 @parent_issue.nil? ? nil : @parent_issue.id
1136 @parent_issue.nil? ? nil : @parent_issue.id
1132 else
1137 else
1133 parent_id
1138 parent_id
1134 end
1139 end
1135 end
1140 end
1136
1141
1137 # Returns true if issue's project is a valid
1142 # Returns true if issue's project is a valid
1138 # parent issue project
1143 # parent issue project
1139 def valid_parent_project?(issue=parent)
1144 def valid_parent_project?(issue=parent)
1140 return true if issue.nil? || issue.project_id == project_id
1145 return true if issue.nil? || issue.project_id == project_id
1141
1146
1142 case Setting.cross_project_subtasks
1147 case Setting.cross_project_subtasks
1143 when 'system'
1148 when 'system'
1144 true
1149 true
1145 when 'tree'
1150 when 'tree'
1146 issue.project.root == project.root
1151 issue.project.root == project.root
1147 when 'hierarchy'
1152 when 'hierarchy'
1148 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1153 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1149 when 'descendants'
1154 when 'descendants'
1150 issue.project.is_or_is_ancestor_of?(project)
1155 issue.project.is_or_is_ancestor_of?(project)
1151 else
1156 else
1152 false
1157 false
1153 end
1158 end
1154 end
1159 end
1155
1160
1156 # Extracted from the ReportsController.
1161 # Extracted from the ReportsController.
1157 def self.by_tracker(project)
1162 def self.by_tracker(project)
1158 count_and_group_by(:project => project,
1163 count_and_group_by(:project => project,
1159 :field => 'tracker_id',
1164 :field => 'tracker_id',
1160 :joins => Tracker.table_name)
1165 :joins => Tracker.table_name)
1161 end
1166 end
1162
1167
1163 def self.by_version(project)
1168 def self.by_version(project)
1164 count_and_group_by(:project => project,
1169 count_and_group_by(:project => project,
1165 :field => 'fixed_version_id',
1170 :field => 'fixed_version_id',
1166 :joins => Version.table_name)
1171 :joins => Version.table_name)
1167 end
1172 end
1168
1173
1169 def self.by_priority(project)
1174 def self.by_priority(project)
1170 count_and_group_by(:project => project,
1175 count_and_group_by(:project => project,
1171 :field => 'priority_id',
1176 :field => 'priority_id',
1172 :joins => IssuePriority.table_name)
1177 :joins => IssuePriority.table_name)
1173 end
1178 end
1174
1179
1175 def self.by_category(project)
1180 def self.by_category(project)
1176 count_and_group_by(:project => project,
1181 count_and_group_by(:project => project,
1177 :field => 'category_id',
1182 :field => 'category_id',
1178 :joins => IssueCategory.table_name)
1183 :joins => IssueCategory.table_name)
1179 end
1184 end
1180
1185
1181 def self.by_assigned_to(project)
1186 def self.by_assigned_to(project)
1182 count_and_group_by(:project => project,
1187 count_and_group_by(:project => project,
1183 :field => 'assigned_to_id',
1188 :field => 'assigned_to_id',
1184 :joins => User.table_name)
1189 :joins => User.table_name)
1185 end
1190 end
1186
1191
1187 def self.by_author(project)
1192 def self.by_author(project)
1188 count_and_group_by(:project => project,
1193 count_and_group_by(:project => project,
1189 :field => 'author_id',
1194 :field => 'author_id',
1190 :joins => User.table_name)
1195 :joins => User.table_name)
1191 end
1196 end
1192
1197
1193 def self.by_subproject(project)
1198 def self.by_subproject(project)
1194 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1199 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1195 s.is_closed as closed,
1200 s.is_closed as closed,
1196 #{Issue.table_name}.project_id as project_id,
1201 #{Issue.table_name}.project_id as project_id,
1197 count(#{Issue.table_name}.id) as total
1202 count(#{Issue.table_name}.id) as total
1198 from
1203 from
1199 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1204 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1200 where
1205 where
1201 #{Issue.table_name}.status_id=s.id
1206 #{Issue.table_name}.status_id=s.id
1202 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1207 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1203 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1208 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1204 and #{Issue.table_name}.project_id <> #{project.id}
1209 and #{Issue.table_name}.project_id <> #{project.id}
1205 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1210 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1206 end
1211 end
1207 # End ReportsController extraction
1212 # End ReportsController extraction
1208
1213
1209 # Returns a scope of projects that user can assign the issue to
1214 # Returns a scope of projects that user can assign the issue to
1210 def allowed_target_projects(user=User.current)
1215 def allowed_target_projects(user=User.current)
1211 if new_record?
1216 if new_record?
1212 Project.where(Project.allowed_to_condition(user, :add_issues))
1217 Project.where(Project.allowed_to_condition(user, :add_issues))
1213 else
1218 else
1214 self.class.allowed_target_projects_on_move(user)
1219 self.class.allowed_target_projects_on_move(user)
1215 end
1220 end
1216 end
1221 end
1217
1222
1218 # Returns a scope of projects that user can move issues to
1223 # Returns a scope of projects that user can move issues to
1219 def self.allowed_target_projects_on_move(user=User.current)
1224 def self.allowed_target_projects_on_move(user=User.current)
1220 Project.where(Project.allowed_to_condition(user, :move_issues))
1225 Project.where(Project.allowed_to_condition(user, :move_issues))
1221 end
1226 end
1222
1227
1223 private
1228 private
1224
1229
1225 def after_project_change
1230 def after_project_change
1226 # Update project_id on related time entries
1231 # Update project_id on related time entries
1227 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1232 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1228
1233
1229 # Delete issue relations
1234 # Delete issue relations
1230 unless Setting.cross_project_issue_relations?
1235 unless Setting.cross_project_issue_relations?
1231 relations_from.clear
1236 relations_from.clear
1232 relations_to.clear
1237 relations_to.clear
1233 end
1238 end
1234
1239
1235 # Move subtasks that were in the same project
1240 # Move subtasks that were in the same project
1236 children.each do |child|
1241 children.each do |child|
1237 next unless child.project_id == project_id_was
1242 next unless child.project_id == project_id_was
1238 # Change project and keep project
1243 # Change project and keep project
1239 child.send :project=, project, true
1244 child.send :project=, project, true
1240 unless child.save
1245 unless child.save
1241 raise ActiveRecord::Rollback
1246 raise ActiveRecord::Rollback
1242 end
1247 end
1243 end
1248 end
1244 end
1249 end
1245
1250
1246 # Callback for after the creation of an issue by copy
1251 # Callback for after the creation of an issue by copy
1247 # * adds a "copied to" relation with the copied issue
1252 # * adds a "copied to" relation with the copied issue
1248 # * copies subtasks from the copied issue
1253 # * copies subtasks from the copied issue
1249 def after_create_from_copy
1254 def after_create_from_copy
1250 return unless copy? && !@after_create_from_copy_handled
1255 return unless copy? && !@after_create_from_copy_handled
1251
1256
1252 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1257 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1253 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1258 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1254 unless relation.save
1259 unless relation.save
1255 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1260 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1256 end
1261 end
1257 end
1262 end
1258
1263
1259 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1264 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1260 copy_options = (@copy_options || {}).merge(:subtasks => false)
1265 copy_options = (@copy_options || {}).merge(:subtasks => false)
1261 copied_issue_ids = {@copied_from.id => self.id}
1266 copied_issue_ids = {@copied_from.id => self.id}
1262 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1267 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1263 # Do not copy self when copying an issue as a descendant of the copied issue
1268 # Do not copy self when copying an issue as a descendant of the copied issue
1264 next if child == self
1269 next if child == self
1265 # Do not copy subtasks of issues that were not copied
1270 # Do not copy subtasks of issues that were not copied
1266 next unless copied_issue_ids[child.parent_id]
1271 next unless copied_issue_ids[child.parent_id]
1267 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1272 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1268 unless child.visible?
1273 unless child.visible?
1269 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1274 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1270 next
1275 next
1271 end
1276 end
1272 copy = Issue.new.copy_from(child, copy_options)
1277 copy = Issue.new.copy_from(child, copy_options)
1273 copy.author = author
1278 copy.author = author
1274 copy.project = project
1279 copy.project = project
1275 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1280 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1276 unless copy.save
1281 unless copy.save
1277 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1282 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1278 next
1283 next
1279 end
1284 end
1280 copied_issue_ids[child.id] = copy.id
1285 copied_issue_ids[child.id] = copy.id
1281 end
1286 end
1282 end
1287 end
1283 @after_create_from_copy_handled = true
1288 @after_create_from_copy_handled = true
1284 end
1289 end
1285
1290
1286 def update_nested_set_attributes
1291 def update_nested_set_attributes
1287 if root_id.nil?
1292 if root_id.nil?
1288 # issue was just created
1293 # issue was just created
1289 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1294 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1290 set_default_left_and_right
1295 set_default_left_and_right
1291 Issue.where(["id = ?", id]).
1296 Issue.where(["id = ?", id]).
1292 update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt])
1297 update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt])
1293 if @parent_issue
1298 if @parent_issue
1294 move_to_child_of(@parent_issue)
1299 move_to_child_of(@parent_issue)
1295 end
1300 end
1296 elsif parent_issue_id != parent_id
1301 elsif parent_issue_id != parent_id
1297 update_nested_set_attributes_on_parent_change
1302 update_nested_set_attributes_on_parent_change
1298 end
1303 end
1299 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1304 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1300 end
1305 end
1301
1306
1302 # Updates the nested set for when an existing issue is moved
1307 # Updates the nested set for when an existing issue is moved
1303 def update_nested_set_attributes_on_parent_change
1308 def update_nested_set_attributes_on_parent_change
1304 former_parent_id = parent_id
1309 former_parent_id = parent_id
1305 # moving an existing issue
1310 # moving an existing issue
1306 if @parent_issue && @parent_issue.root_id == root_id
1311 if @parent_issue && @parent_issue.root_id == root_id
1307 # inside the same tree
1312 # inside the same tree
1308 move_to_child_of(@parent_issue)
1313 move_to_child_of(@parent_issue)
1309 else
1314 else
1310 # to another tree
1315 # to another tree
1311 unless root?
1316 unless root?
1312 move_to_right_of(root)
1317 move_to_right_of(root)
1313 end
1318 end
1314 old_root_id = root_id
1319 old_root_id = root_id
1315 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1320 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1316 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1321 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1317 offset = target_maxright + 1 - lft
1322 offset = target_maxright + 1 - lft
1318 Issue.where(["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]).
1323 Issue.where(["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt]).
1319 update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1324 update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset])
1320 self[left_column_name] = lft + offset
1325 self[left_column_name] = lft + offset
1321 self[right_column_name] = rgt + offset
1326 self[right_column_name] = rgt + offset
1322 if @parent_issue
1327 if @parent_issue
1323 move_to_child_of(@parent_issue)
1328 move_to_child_of(@parent_issue)
1324 end
1329 end
1325 end
1330 end
1326 # delete invalid relations of all descendants
1331 # delete invalid relations of all descendants
1327 self_and_descendants.each do |issue|
1332 self_and_descendants.each do |issue|
1328 issue.relations.each do |relation|
1333 issue.relations.each do |relation|
1329 relation.destroy unless relation.valid?
1334 relation.destroy unless relation.valid?
1330 end
1335 end
1331 end
1336 end
1332 # update former parent
1337 # update former parent
1333 recalculate_attributes_for(former_parent_id) if former_parent_id
1338 recalculate_attributes_for(former_parent_id) if former_parent_id
1334 end
1339 end
1335
1340
1336 def update_parent_attributes
1341 def update_parent_attributes
1337 recalculate_attributes_for(parent_id) if parent_id
1342 recalculate_attributes_for(parent_id) if parent_id
1338 end
1343 end
1339
1344
1340 def recalculate_attributes_for(issue_id)
1345 def recalculate_attributes_for(issue_id)
1341 if issue_id && p = Issue.find_by_id(issue_id)
1346 if issue_id && p = Issue.find_by_id(issue_id)
1342 # priority = highest priority of children
1347 # priority = highest priority of children
1343 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1348 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1344 p.priority = IssuePriority.find_by_position(priority_position)
1349 p.priority = IssuePriority.find_by_position(priority_position)
1345 end
1350 end
1346
1351
1347 # start/due dates = lowest/highest dates of children
1352 # start/due dates = lowest/highest dates of children
1348 p.start_date = p.children.minimum(:start_date)
1353 p.start_date = p.children.minimum(:start_date)
1349 p.due_date = p.children.maximum(:due_date)
1354 p.due_date = p.children.maximum(:due_date)
1350 if p.start_date && p.due_date && p.due_date < p.start_date
1355 if p.start_date && p.due_date && p.due_date < p.start_date
1351 p.start_date, p.due_date = p.due_date, p.start_date
1356 p.start_date, p.due_date = p.due_date, p.start_date
1352 end
1357 end
1353
1358
1354 # done ratio = weighted average ratio of leaves
1359 # done ratio = weighted average ratio of leaves
1355 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1360 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1356 leaves_count = p.leaves.count
1361 leaves_count = p.leaves.count
1357 if leaves_count > 0
1362 if leaves_count > 0
1358 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1363 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1359 if average == 0
1364 if average == 0
1360 average = 1
1365 average = 1
1361 end
1366 end
1362 done = p.leaves.joins(:status).
1367 done = p.leaves.joins(:status).
1363 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1368 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1364 "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1369 "* (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1365 progress = done / (average * leaves_count)
1370 progress = done / (average * leaves_count)
1366 p.done_ratio = progress.round
1371 p.done_ratio = progress.round
1367 end
1372 end
1368 end
1373 end
1369
1374
1370 # estimate = sum of leaves estimates
1375 # estimate = sum of leaves estimates
1371 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1376 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1372 p.estimated_hours = nil if p.estimated_hours == 0.0
1377 p.estimated_hours = nil if p.estimated_hours == 0.0
1373
1378
1374 # ancestors will be recursively updated
1379 # ancestors will be recursively updated
1375 p.save(:validate => false)
1380 p.save(:validate => false)
1376 end
1381 end
1377 end
1382 end
1378
1383
1379 # Update issues so their versions are not pointing to a
1384 # Update issues so their versions are not pointing to a
1380 # fixed_version that is not shared with the issue's project
1385 # fixed_version that is not shared with the issue's project
1381 def self.update_versions(conditions=nil)
1386 def self.update_versions(conditions=nil)
1382 # Only need to update issues with a fixed_version from
1387 # Only need to update issues with a fixed_version from
1383 # a different project and that is not systemwide shared
1388 # a different project and that is not systemwide shared
1384 Issue.includes(:project, :fixed_version).
1389 Issue.includes(:project, :fixed_version).
1385 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1390 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1386 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1391 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1387 " AND #{Version.table_name}.sharing <> 'system'").
1392 " AND #{Version.table_name}.sharing <> 'system'").
1388 where(conditions).each do |issue|
1393 where(conditions).each do |issue|
1389 next if issue.project.nil? || issue.fixed_version.nil?
1394 next if issue.project.nil? || issue.fixed_version.nil?
1390 unless issue.project.shared_versions.include?(issue.fixed_version)
1395 unless issue.project.shared_versions.include?(issue.fixed_version)
1391 issue.init_journal(User.current)
1396 issue.init_journal(User.current)
1392 issue.fixed_version = nil
1397 issue.fixed_version = nil
1393 issue.save
1398 issue.save
1394 end
1399 end
1395 end
1400 end
1396 end
1401 end
1397
1402
1398 # Callback on file attachment
1403 # Callback on file attachment
1399 def attachment_added(obj)
1404 def attachment_added(obj)
1400 if @current_journal && !obj.new_record?
1405 if @current_journal && !obj.new_record?
1401 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1406 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1402 end
1407 end
1403 end
1408 end
1404
1409
1405 # Callback on attachment deletion
1410 # Callback on attachment deletion
1406 def attachment_removed(obj)
1411 def attachment_removed(obj)
1407 if @current_journal && !obj.new_record?
1412 if @current_journal && !obj.new_record?
1408 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1413 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1409 @current_journal.save
1414 @current_journal.save
1410 end
1415 end
1411 end
1416 end
1412
1417
1413 # Default assignment based on category
1418 # Default assignment based on category
1414 def default_assign
1419 def default_assign
1415 if assigned_to.nil? && category && category.assigned_to
1420 if assigned_to.nil? && category && category.assigned_to
1416 self.assigned_to = category.assigned_to
1421 self.assigned_to = category.assigned_to
1417 end
1422 end
1418 end
1423 end
1419
1424
1420 # Updates start/due dates of following issues
1425 # Updates start/due dates of following issues
1421 def reschedule_following_issues
1426 def reschedule_following_issues
1422 if start_date_changed? || due_date_changed?
1427 if start_date_changed? || due_date_changed?
1423 relations_from.each do |relation|
1428 relations_from.each do |relation|
1424 relation.set_issue_to_dates
1429 relation.set_issue_to_dates
1425 end
1430 end
1426 end
1431 end
1427 end
1432 end
1428
1433
1429 # Closes duplicates if the issue is being closed
1434 # Closes duplicates if the issue is being closed
1430 def close_duplicates
1435 def close_duplicates
1431 if closing?
1436 if closing?
1432 duplicates.each do |duplicate|
1437 duplicates.each do |duplicate|
1433 # Reload is need in case the duplicate was updated by a previous duplicate
1438 # Reload is need in case the duplicate was updated by a previous duplicate
1434 duplicate.reload
1439 duplicate.reload
1435 # Don't re-close it if it's already closed
1440 # Don't re-close it if it's already closed
1436 next if duplicate.closed?
1441 next if duplicate.closed?
1437 # Same user and notes
1442 # Same user and notes
1438 if @current_journal
1443 if @current_journal
1439 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1444 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1440 end
1445 end
1441 duplicate.update_attribute :status, self.status
1446 duplicate.update_attribute :status, self.status
1442 end
1447 end
1443 end
1448 end
1444 end
1449 end
1445
1450
1446 # Make sure updated_on is updated when adding a note and set updated_on now
1451 # Make sure updated_on is updated when adding a note and set updated_on now
1447 # so we can set closed_on with the same value on closing
1452 # so we can set closed_on with the same value on closing
1448 def force_updated_on_change
1453 def force_updated_on_change
1449 if @current_journal || changed?
1454 if @current_journal || changed?
1450 self.updated_on = current_time_from_proper_timezone
1455 self.updated_on = current_time_from_proper_timezone
1451 if new_record?
1456 if new_record?
1452 self.created_on = updated_on
1457 self.created_on = updated_on
1453 end
1458 end
1454 end
1459 end
1455 end
1460 end
1456
1461
1457 # Callback for setting closed_on when the issue is closed.
1462 # Callback for setting closed_on when the issue is closed.
1458 # The closed_on attribute stores the time of the last closing
1463 # The closed_on attribute stores the time of the last closing
1459 # and is preserved when the issue is reopened.
1464 # and is preserved when the issue is reopened.
1460 def update_closed_on
1465 def update_closed_on
1461 if closing? || (new_record? && closed?)
1466 if closing? || (new_record? && closed?)
1462 self.closed_on = updated_on
1467 self.closed_on = updated_on
1463 end
1468 end
1464 end
1469 end
1465
1470
1466 # Saves the changes in a Journal
1471 # Saves the changes in a Journal
1467 # Called after_save
1472 # Called after_save
1468 def create_journal
1473 def create_journal
1469 if @current_journal
1474 if @current_journal
1470 # attributes changes
1475 # attributes changes
1471 if @attributes_before_change
1476 if @attributes_before_change
1472 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1477 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1473 before = @attributes_before_change[c]
1478 before = @attributes_before_change[c]
1474 after = send(c)
1479 after = send(c)
1475 next if before == after || (before.blank? && after.blank?)
1480 next if before == after || (before.blank? && after.blank?)
1476 @current_journal.details << JournalDetail.new(:property => 'attr',
1481 @current_journal.details << JournalDetail.new(:property => 'attr',
1477 :prop_key => c,
1482 :prop_key => c,
1478 :old_value => before,
1483 :old_value => before,
1479 :value => after)
1484 :value => after)
1480 }
1485 }
1481 end
1486 end
1482 if @custom_values_before_change
1487 if @custom_values_before_change
1483 # custom fields changes
1488 # custom fields changes
1484 custom_field_values.each {|c|
1489 custom_field_values.each {|c|
1485 before = @custom_values_before_change[c.custom_field_id]
1490 before = @custom_values_before_change[c.custom_field_id]
1486 after = c.value
1491 after = c.value
1487 next if before == after || (before.blank? && after.blank?)
1492 next if before == after || (before.blank? && after.blank?)
1488
1493
1489 if before.is_a?(Array) || after.is_a?(Array)
1494 if before.is_a?(Array) || after.is_a?(Array)
1490 before = [before] unless before.is_a?(Array)
1495 before = [before] unless before.is_a?(Array)
1491 after = [after] unless after.is_a?(Array)
1496 after = [after] unless after.is_a?(Array)
1492
1497
1493 # values removed
1498 # values removed
1494 (before - after).reject(&:blank?).each do |value|
1499 (before - after).reject(&:blank?).each do |value|
1495 @current_journal.details << JournalDetail.new(:property => 'cf',
1500 @current_journal.details << JournalDetail.new(:property => 'cf',
1496 :prop_key => c.custom_field_id,
1501 :prop_key => c.custom_field_id,
1497 :old_value => value,
1502 :old_value => value,
1498 :value => nil)
1503 :value => nil)
1499 end
1504 end
1500 # values added
1505 # values added
1501 (after - before).reject(&:blank?).each do |value|
1506 (after - before).reject(&:blank?).each do |value|
1502 @current_journal.details << JournalDetail.new(:property => 'cf',
1507 @current_journal.details << JournalDetail.new(:property => 'cf',
1503 :prop_key => c.custom_field_id,
1508 :prop_key => c.custom_field_id,
1504 :old_value => nil,
1509 :old_value => nil,
1505 :value => value)
1510 :value => value)
1506 end
1511 end
1507 else
1512 else
1508 @current_journal.details << JournalDetail.new(:property => 'cf',
1513 @current_journal.details << JournalDetail.new(:property => 'cf',
1509 :prop_key => c.custom_field_id,
1514 :prop_key => c.custom_field_id,
1510 :old_value => before,
1515 :old_value => before,
1511 :value => after)
1516 :value => after)
1512 end
1517 end
1513 }
1518 }
1514 end
1519 end
1515 @current_journal.save
1520 @current_journal.save
1516 # reset current journal
1521 # reset current journal
1517 init_journal @current_journal.user, @current_journal.notes
1522 init_journal @current_journal.user, @current_journal.notes
1518 end
1523 end
1519 end
1524 end
1520
1525
1521 def send_notification
1526 def send_notification
1522 if Setting.notified_events.include?('issue_added')
1527 if Setting.notified_events.include?('issue_added')
1523 Mailer.deliver_issue_add(self)
1528 Mailer.deliver_issue_add(self)
1524 end
1529 end
1525 end
1530 end
1526
1531
1527 # Stores the previous assignee so we can still have access
1532 # Stores the previous assignee so we can still have access
1528 # to it during after_save callbacks (assigned_to_id_was is reset)
1533 # to it during after_save callbacks (assigned_to_id_was is reset)
1529 def set_assigned_to_was
1534 def set_assigned_to_was
1530 @previous_assigned_to_id = assigned_to_id_was
1535 @previous_assigned_to_id = assigned_to_id_was
1531 end
1536 end
1532
1537
1533 # Clears the previous assignee at the end of after_save callbacks
1538 # Clears the previous assignee at the end of after_save callbacks
1534 def clear_assigned_to_was
1539 def clear_assigned_to_was
1535 @assigned_to_was = nil
1540 @assigned_to_was = nil
1536 @previous_assigned_to_id = nil
1541 @previous_assigned_to_id = nil
1537 end
1542 end
1538
1543
1539 # Query generator for selecting groups of issue counts for a project
1544 # Query generator for selecting groups of issue counts for a project
1540 # based on specific criteria
1545 # based on specific criteria
1541 #
1546 #
1542 # Options
1547 # Options
1543 # * project - Project to search in.
1548 # * project - Project to search in.
1544 # * field - String. Issue field to key off of in the grouping.
1549 # * field - String. Issue field to key off of in the grouping.
1545 # * joins - String. The table name to join against.
1550 # * joins - String. The table name to join against.
1546 def self.count_and_group_by(options)
1551 def self.count_and_group_by(options)
1547 project = options.delete(:project)
1552 project = options.delete(:project)
1548 select_field = options.delete(:field)
1553 select_field = options.delete(:field)
1549 joins = options.delete(:joins)
1554 joins = options.delete(:joins)
1550
1555
1551 where = "#{Issue.table_name}.#{select_field}=j.id"
1556 where = "#{Issue.table_name}.#{select_field}=j.id"
1552
1557
1553 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1558 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1554 s.is_closed as closed,
1559 s.is_closed as closed,
1555 j.id as #{select_field},
1560 j.id as #{select_field},
1556 count(#{Issue.table_name}.id) as total
1561 count(#{Issue.table_name}.id) as total
1557 from
1562 from
1558 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1563 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1559 where
1564 where
1560 #{Issue.table_name}.status_id=s.id
1565 #{Issue.table_name}.status_id=s.id
1561 and #{where}
1566 and #{where}
1562 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1567 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1563 and #{visible_condition(User.current, :project => project)}
1568 and #{visible_condition(User.current, :project => project)}
1564 group by s.id, s.is_closed, j.id")
1569 group by s.id, s.is_closed, j.id")
1565 end
1570 end
1566 end
1571 end
@@ -1,250 +1,262
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class ContextMenusControllerTest < ActionController::TestCase
20 class ContextMenusControllerTest < ActionController::TestCase
21 fixtures :projects,
21 fixtures :projects,
22 :trackers,
22 :trackers,
23 :projects_trackers,
23 :projects_trackers,
24 :roles,
24 :roles,
25 :member_roles,
25 :member_roles,
26 :members,
26 :members,
27 :enabled_modules,
27 :enabled_modules,
28 :workflows,
28 :workflows,
29 :journals, :journal_details,
29 :journals, :journal_details,
30 :versions,
30 :versions,
31 :issues, :issue_statuses, :issue_categories,
31 :issues, :issue_statuses, :issue_categories,
32 :users,
32 :users,
33 :enumerations,
33 :enumerations,
34 :time_entries
34 :time_entries
35
35
36 def test_context_menu_one_issue
36 def test_context_menu_one_issue
37 @request.session[:user_id] = 2
37 @request.session[:user_id] = 2
38 get :issues, :ids => [1]
38 get :issues, :ids => [1]
39 assert_response :success
39 assert_response :success
40 assert_template 'context_menus/issues'
40 assert_template 'context_menus/issues'
41
41
42 assert_select 'a.icon-edit[href=?]', '/issues/1/edit', :text => 'Edit'
42 assert_select 'a.icon-edit[href=?]', '/issues/1/edit', :text => 'Edit'
43 assert_select 'a.icon-copy[href=?]', '/projects/ecookbook/issues/1/copy', :text => 'Copy'
43 assert_select 'a.icon-copy[href=?]', '/projects/ecookbook/issues/1/copy', :text => 'Copy'
44 assert_select 'a.icon-del[href=?]', '/issues?ids%5B%5D=1', :text => 'Delete'
44 assert_select 'a.icon-del[href=?]', '/issues?ids%5B%5D=1', :text => 'Delete'
45
45
46 # Statuses
46 # Statuses
47 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bstatus_id%5D=5', :text => 'Closed'
47 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bstatus_id%5D=5', :text => 'Closed'
48 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bpriority_id%5D=8', :text => 'Immediate'
48 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bpriority_id%5D=8', :text => 'Immediate'
49 # No inactive priorities
49 # No inactive priorities
50 assert_select 'a', :text => /Inactive Priority/, :count => 0
50 assert_select 'a', :text => /Inactive Priority/, :count => 0
51 # Versions
51 # Versions
52 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=3', :text => '2.0'
52 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=3', :text => '2.0'
53 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=4', :text => 'eCookbook Subproject 1 - 2.0'
53 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bfixed_version_id%5D=4', :text => 'eCookbook Subproject 1 - 2.0'
54 # Assignees
54 # Assignees
55 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=3', :text => 'Dave Lopper'
55 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=3', :text => 'Dave Lopper'
56 end
56 end
57
57
58 def test_context_menu_one_issue_by_anonymous
58 def test_context_menu_one_issue_by_anonymous
59 get :issues, :ids => [1]
59 get :issues, :ids => [1]
60 assert_response :success
60 assert_response :success
61 assert_template 'context_menus/issues'
61 assert_template 'context_menus/issues'
62 assert_tag :tag => 'a', :content => 'Delete',
62 assert_tag :tag => 'a', :content => 'Delete',
63 :attributes => { :href => '#',
63 :attributes => { :href => '#',
64 :class => 'icon-del disabled' }
64 :class => 'icon-del disabled' }
65 end
65 end
66
66
67 def test_context_menu_multiple_issues_of_same_project
67 def test_context_menu_multiple_issues_of_same_project
68 @request.session[:user_id] = 2
68 @request.session[:user_id] = 2
69 get :issues, :ids => [1, 2]
69 get :issues, :ids => [1, 2]
70 assert_response :success
70 assert_response :success
71 assert_template 'context_menus/issues'
71 assert_template 'context_menus/issues'
72 assert_not_nil assigns(:issues)
72 assert_not_nil assigns(:issues)
73 assert_equal [1, 2], assigns(:issues).map(&:id).sort
73 assert_equal [1, 2], assigns(:issues).map(&:id).sort
74
74
75 ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
75 ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
76
76
77 assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit'
77 assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit'
78 assert_select 'a.icon-copy[href=?]', "/issues/bulk_edit?copy=1&amp;#{ids}", :text => 'Copy'
78 assert_select 'a.icon-copy[href=?]', "/issues/bulk_edit?copy=1&amp;#{ids}", :text => 'Copy'
79 assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete'
79 assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete'
80
80
81 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5", :text => 'Closed'
81 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5", :text => 'Closed'
82 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8", :text => 'Immediate'
82 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8", :text => 'Immediate'
83 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=3", :text => 'Dave Lopper'
83 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=3", :text => 'Dave Lopper'
84 end
84 end
85
85
86 def test_context_menu_multiple_issues_of_different_projects
86 def test_context_menu_multiple_issues_of_different_projects
87 @request.session[:user_id] = 2
87 @request.session[:user_id] = 2
88 get :issues, :ids => [1, 2, 6]
88 get :issues, :ids => [1, 2, 6]
89 assert_response :success
89 assert_response :success
90 assert_template 'context_menus/issues'
90 assert_template 'context_menus/issues'
91 assert_not_nil assigns(:issues)
91 assert_not_nil assigns(:issues)
92 assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort
92 assert_equal [1, 2, 6], assigns(:issues).map(&:id).sort
93
93
94 ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
94 ids = assigns(:issues).map(&:id).sort.map {|i| "ids%5B%5D=#{i}"}.join('&amp;')
95
95
96 assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit'
96 assert_select 'a.icon-edit[href=?]', "/issues/bulk_edit?#{ids}", :text => 'Edit'
97 assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete'
97 assert_select 'a.icon-del[href=?]', "/issues?#{ids}", :text => 'Delete'
98
98
99 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5", :text => 'Closed'
99 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bstatus_id%5D=5", :text => 'Closed'
100 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8", :text => 'Immediate'
100 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bpriority_id%5D=8", :text => 'Immediate'
101 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=2", :text => 'John Smith'
101 assert_select 'a[href=?]', "/issues/bulk_update?#{ids}&amp;issue%5Bassigned_to_id%5D=2", :text => 'John Smith'
102 end
102 end
103
103
104 def test_context_menu_should_include_list_custom_fields
104 def test_context_menu_should_include_list_custom_fields
105 field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
105 field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
106 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
106 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
107 @request.session[:user_id] = 2
107 @request.session[:user_id] = 2
108 get :issues, :ids => [1]
108 get :issues, :ids => [1]
109
109
110 assert_select "li.cf_#{field.id}" do
110 assert_select "li.cf_#{field.id}" do
111 assert_select 'a[href=#]', :text => 'List'
111 assert_select 'a[href=#]', :text => 'List'
112 assert_select 'ul' do
112 assert_select 'ul' do
113 assert_select 'a', 3
113 assert_select 'a', 3
114 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo", :text => 'Foo'
114 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=Foo", :text => 'Foo'
115 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
115 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
116 end
116 end
117 end
117 end
118 end
118 end
119
119
120 def test_context_menu_should_not_include_null_value_for_required_custom_fields
120 def test_context_menu_should_not_include_null_value_for_required_custom_fields
121 field = IssueCustomField.create!(:name => 'List', :is_required => true, :field_format => 'list',
121 field = IssueCustomField.create!(:name => 'List', :is_required => true, :field_format => 'list',
122 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
122 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
123 @request.session[:user_id] = 2
123 @request.session[:user_id] = 2
124 get :issues, :ids => [1, 2]
124 get :issues, :ids => [1, 2]
125
125
126 assert_select "li.cf_#{field.id}" do
126 assert_select "li.cf_#{field.id}" do
127 assert_select 'a[href=#]', :text => 'List'
127 assert_select 'a[href=#]', :text => 'List'
128 assert_select 'ul' do
128 assert_select 'ul' do
129 assert_select 'a', 2
129 assert_select 'a', 2
130 assert_select 'a', :text => 'none', :count => 0
130 assert_select 'a', :text => 'none', :count => 0
131 end
131 end
132 end
132 end
133 end
133 end
134
134
135 def test_context_menu_on_single_issue_should_select_current_custom_field_value
135 def test_context_menu_on_single_issue_should_select_current_custom_field_value
136 field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
136 field = IssueCustomField.create!(:name => 'List', :field_format => 'list',
137 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
137 :possible_values => ['Foo', 'Bar'], :is_for_all => true, :tracker_ids => [1, 2, 3])
138 issue = Issue.find(1)
138 issue = Issue.find(1)
139 issue.custom_field_values = {field.id => 'Bar'}
139 issue.custom_field_values = {field.id => 'Bar'}
140 issue.save!
140 issue.save!
141 @request.session[:user_id] = 2
141 @request.session[:user_id] = 2
142 get :issues, :ids => [1]
142 get :issues, :ids => [1]
143
143
144 assert_select "li.cf_#{field.id}" do
144 assert_select "li.cf_#{field.id}" do
145 assert_select 'a[href=#]', :text => 'List'
145 assert_select 'a[href=#]', :text => 'List'
146 assert_select 'ul' do
146 assert_select 'ul' do
147 assert_select 'a', 3
147 assert_select 'a', 3
148 assert_select 'a.icon-checked', :text => 'Bar'
148 assert_select 'a.icon-checked', :text => 'Bar'
149 end
149 end
150 end
150 end
151 end
151 end
152
152
153 def test_context_menu_should_include_bool_custom_fields
153 def test_context_menu_should_include_bool_custom_fields
154 field = IssueCustomField.create!(:name => 'Bool', :field_format => 'bool',
154 field = IssueCustomField.create!(:name => 'Bool', :field_format => 'bool',
155 :is_for_all => true, :tracker_ids => [1, 2, 3])
155 :is_for_all => true, :tracker_ids => [1, 2, 3])
156 @request.session[:user_id] = 2
156 @request.session[:user_id] = 2
157 get :issues, :ids => [1]
157 get :issues, :ids => [1]
158
158
159 assert_select "li.cf_#{field.id}" do
159 assert_select "li.cf_#{field.id}" do
160 assert_select 'a[href=#]', :text => 'Bool'
160 assert_select 'a[href=#]', :text => 'Bool'
161 assert_select 'ul' do
161 assert_select 'ul' do
162 assert_select 'a', 3
162 assert_select 'a', 3
163 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=0", :text => 'No'
163 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=0", :text => 'No'
164 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1", :text => 'Yes'
164 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=1", :text => 'Yes'
165 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
165 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
166 end
166 end
167 end
167 end
168 end
168 end
169
169
170 def test_context_menu_should_include_user_custom_fields
170 def test_context_menu_should_include_user_custom_fields
171 field = IssueCustomField.create!(:name => 'User', :field_format => 'user',
171 field = IssueCustomField.create!(:name => 'User', :field_format => 'user',
172 :is_for_all => true, :tracker_ids => [1, 2, 3])
172 :is_for_all => true, :tracker_ids => [1, 2, 3])
173 @request.session[:user_id] = 2
173 @request.session[:user_id] = 2
174 get :issues, :ids => [1]
174 get :issues, :ids => [1]
175
175
176 assert_select "li.cf_#{field.id}" do
176 assert_select "li.cf_#{field.id}" do
177 assert_select 'a[href=#]', :text => 'User'
177 assert_select 'a[href=#]', :text => 'User'
178 assert_select 'ul' do
178 assert_select 'ul' do
179 assert_select 'a', Project.find(1).members.count + 1
179 assert_select 'a', Project.find(1).members.count + 1
180 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2", :text => 'John Smith'
180 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=2", :text => 'John Smith'
181 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
181 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
182 end
182 end
183 end
183 end
184 end
184 end
185
185
186 def test_context_menu_should_include_version_custom_fields
186 def test_context_menu_should_include_version_custom_fields
187 field = IssueCustomField.create!(:name => 'Version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1, 2, 3])
187 field = IssueCustomField.create!(:name => 'Version', :field_format => 'version', :is_for_all => true, :tracker_ids => [1, 2, 3])
188 @request.session[:user_id] = 2
188 @request.session[:user_id] = 2
189 get :issues, :ids => [1]
189 get :issues, :ids => [1]
190
190
191 assert_select "li.cf_#{field.id}" do
191 assert_select "li.cf_#{field.id}" do
192 assert_select 'a[href=#]', :text => 'Version'
192 assert_select 'a[href=#]', :text => 'Version'
193 assert_select 'ul' do
193 assert_select 'ul' do
194 assert_select 'a', Project.find(1).shared_versions.count + 1
194 assert_select 'a', Project.find(1).shared_versions.count + 1
195 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3", :text => '2.0'
195 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=3", :text => '2.0'
196 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
196 assert_select 'a[href=?]', "/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bcustom_field_values%5D%5B#{field.id}%5D=__none__", :text => 'none'
197 end
197 end
198 end
198 end
199 end
199 end
200
200
201 def test_context_menu_should_show_enabled_custom_fields_for_the_role_only
202 enabled_cf = IssueCustomField.generate!(:field_format => 'bool', :is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
203 disabled_cf = IssueCustomField.generate!(:field_format => 'bool', :is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
204 issue = Issue.generate!(:project_id => 1, :tracker_id => 1)
205
206 @request.session[:user_id] = 2
207 get :issues, :ids => [issue.id]
208
209 assert_select "li.cf_#{enabled_cf.id}"
210 assert_select "li.cf_#{disabled_cf.id}", 0
211 end
212
201 def test_context_menu_by_assignable_user_should_include_assigned_to_me_link
213 def test_context_menu_by_assignable_user_should_include_assigned_to_me_link
202 @request.session[:user_id] = 2
214 @request.session[:user_id] = 2
203 get :issues, :ids => [1]
215 get :issues, :ids => [1]
204 assert_response :success
216 assert_response :success
205 assert_template 'context_menus/issues'
217 assert_template 'context_menus/issues'
206
218
207 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=2', :text => / me /
219 assert_select 'a[href=?]', '/issues/bulk_update?ids%5B%5D=1&amp;issue%5Bassigned_to_id%5D=2', :text => / me /
208 end
220 end
209
221
210 def test_context_menu_should_propose_shared_versions_for_issues_from_different_projects
222 def test_context_menu_should_propose_shared_versions_for_issues_from_different_projects
211 @request.session[:user_id] = 2
223 @request.session[:user_id] = 2
212 version = Version.create!(:name => 'Shared', :sharing => 'system', :project_id => 1)
224 version = Version.create!(:name => 'Shared', :sharing => 'system', :project_id => 1)
213
225
214 get :issues, :ids => [1, 4]
226 get :issues, :ids => [1, 4]
215 assert_response :success
227 assert_response :success
216 assert_template 'context_menus/issues'
228 assert_template 'context_menus/issues'
217
229
218 assert_include version, assigns(:versions)
230 assert_include version, assigns(:versions)
219 assert_select 'a', :text => 'eCookbook - Shared'
231 assert_select 'a', :text => 'eCookbook - Shared'
220 end
232 end
221
233
222 def test_context_menu_with_issue_that_is_not_visible_should_fail
234 def test_context_menu_with_issue_that_is_not_visible_should_fail
223 get :issues, :ids => [1, 4] # issue 4 is not visible
235 get :issues, :ids => [1, 4] # issue 4 is not visible
224 assert_response 302
236 assert_response 302
225 end
237 end
226
238
227 def test_should_respond_with_404_without_ids
239 def test_should_respond_with_404_without_ids
228 get :issues
240 get :issues
229 assert_response 404
241 assert_response 404
230 end
242 end
231
243
232 def test_time_entries_context_menu
244 def test_time_entries_context_menu
233 @request.session[:user_id] = 2
245 @request.session[:user_id] = 2
234 get :time_entries, :ids => [1, 2]
246 get :time_entries, :ids => [1, 2]
235 assert_response :success
247 assert_response :success
236 assert_template 'context_menus/time_entries'
248 assert_template 'context_menus/time_entries'
237
249
238 assert_select 'a:not(.disabled)', :text => 'Edit'
250 assert_select 'a:not(.disabled)', :text => 'Edit'
239 end
251 end
240
252
241 def test_time_entries_context_menu_without_edit_permission
253 def test_time_entries_context_menu_without_edit_permission
242 @request.session[:user_id] = 2
254 @request.session[:user_id] = 2
243 Role.find_by_name('Manager').remove_permission! :edit_time_entries
255 Role.find_by_name('Manager').remove_permission! :edit_time_entries
244
256
245 get :time_entries, :ids => [1, 2]
257 get :time_entries, :ids => [1, 2]
246 assert_response :success
258 assert_response :success
247 assert_template 'context_menus/time_entries'
259 assert_template 'context_menus/time_entries'
248 assert_select 'a.disabled', :text => 'Edit'
260 assert_select 'a.disabled', :text => 'Edit'
249 end
261 end
250 end
262 end
@@ -1,2358 +1,2368
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
2 # Copyright (C) 2006-2014 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class 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 :groups_users,
22 :groups_users,
23 :trackers, :projects_trackers,
23 :trackers, :projects_trackers,
24 :enabled_modules,
24 :enabled_modules,
25 :versions,
25 :versions,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :issue_statuses, :issue_categories, :issue_relations, :workflows,
27 :enumerations,
27 :enumerations,
28 :issues, :journals, :journal_details,
28 :issues, :journals, :journal_details,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
30 :time_entries
30 :time_entries
31
31
32 include Redmine::I18n
32 include Redmine::I18n
33
33
34 def teardown
34 def teardown
35 User.current = nil
35 User.current = nil
36 end
36 end
37
37
38 def test_initialize
38 def test_initialize
39 issue = Issue.new
39 issue = Issue.new
40
40
41 assert_nil issue.project_id
41 assert_nil issue.project_id
42 assert_nil issue.tracker_id
42 assert_nil issue.tracker_id
43 assert_nil issue.author_id
43 assert_nil issue.author_id
44 assert_nil issue.assigned_to_id
44 assert_nil issue.assigned_to_id
45 assert_nil issue.category_id
45 assert_nil issue.category_id
46
46
47 assert_equal IssueStatus.default, issue.status
47 assert_equal IssueStatus.default, issue.status
48 assert_equal IssuePriority.default, issue.priority
48 assert_equal IssuePriority.default, issue.priority
49 end
49 end
50
50
51 def test_create
51 def test_create
52 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
52 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
53 :status_id => 1, :priority => IssuePriority.all.first,
53 :status_id => 1, :priority => IssuePriority.all.first,
54 :subject => 'test_create',
54 :subject => 'test_create',
55 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
55 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
56 assert issue.save
56 assert issue.save
57 issue.reload
57 issue.reload
58 assert_equal 1.5, issue.estimated_hours
58 assert_equal 1.5, issue.estimated_hours
59 end
59 end
60
60
61 def test_create_minimal
61 def test_create_minimal
62 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
62 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
63 :status_id => 1, :priority => IssuePriority.all.first,
63 :status_id => 1, :priority => IssuePriority.all.first,
64 :subject => 'test_create')
64 :subject => 'test_create')
65 assert issue.save
65 assert issue.save
66 assert issue.description.nil?
66 assert issue.description.nil?
67 assert_nil issue.estimated_hours
67 assert_nil issue.estimated_hours
68 end
68 end
69
69
70 def test_start_date_format_should_be_validated
70 def test_start_date_format_should_be_validated
71 set_language_if_valid 'en'
71 set_language_if_valid 'en'
72 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
72 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
73 issue = Issue.new(:start_date => invalid_date)
73 issue = Issue.new(:start_date => invalid_date)
74 assert !issue.valid?
74 assert !issue.valid?
75 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
75 assert_include 'Start date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
76 end
76 end
77 end
77 end
78
78
79 def test_due_date_format_should_be_validated
79 def test_due_date_format_should_be_validated
80 set_language_if_valid 'en'
80 set_language_if_valid 'en'
81 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
81 ['2012', 'ABC', '2012-15-20'].each do |invalid_date|
82 issue = Issue.new(:due_date => invalid_date)
82 issue = Issue.new(:due_date => invalid_date)
83 assert !issue.valid?
83 assert !issue.valid?
84 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
84 assert_include 'Due date is not a valid date', issue.errors.full_messages, "No error found for invalid date #{invalid_date}"
85 end
85 end
86 end
86 end
87
87
88 def test_due_date_lesser_than_start_date_should_not_validate
88 def test_due_date_lesser_than_start_date_should_not_validate
89 set_language_if_valid 'en'
89 set_language_if_valid 'en'
90 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
90 issue = Issue.new(:start_date => '2012-10-06', :due_date => '2012-10-02')
91 assert !issue.valid?
91 assert !issue.valid?
92 assert_include 'Due date must be greater than start date', issue.errors.full_messages
92 assert_include 'Due date must be greater than start date', issue.errors.full_messages
93 end
93 end
94
94
95 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
95 def test_start_date_lesser_than_soonest_start_should_not_validate_on_create
96 issue = Issue.generate(:start_date => '2013-06-04')
96 issue = Issue.generate(:start_date => '2013-06-04')
97 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
97 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
98 assert !issue.valid?
98 assert !issue.valid?
99 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
99 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
100 end
100 end
101
101
102 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
102 def test_start_date_lesser_than_soonest_start_should_not_validate_on_update_if_changed
103 issue = Issue.generate!(:start_date => '2013-06-04')
103 issue = Issue.generate!(:start_date => '2013-06-04')
104 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
104 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
105 issue.start_date = '2013-06-07'
105 issue.start_date = '2013-06-07'
106 assert !issue.valid?
106 assert !issue.valid?
107 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
107 assert_include "Start date cannot be earlier than 06/10/2013 because of preceding issues", issue.errors.full_messages
108 end
108 end
109
109
110 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
110 def test_start_date_lesser_than_soonest_start_should_validate_on_update_if_unchanged
111 issue = Issue.generate!(:start_date => '2013-06-04')
111 issue = Issue.generate!(:start_date => '2013-06-04')
112 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
112 issue.stubs(:soonest_start).returns(Date.parse('2013-06-10'))
113 assert issue.valid?
113 assert issue.valid?
114 end
114 end
115
115
116 def test_estimated_hours_should_be_validated
116 def test_estimated_hours_should_be_validated
117 set_language_if_valid 'en'
117 set_language_if_valid 'en'
118 ['-2'].each do |invalid|
118 ['-2'].each do |invalid|
119 issue = Issue.new(:estimated_hours => invalid)
119 issue = Issue.new(:estimated_hours => invalid)
120 assert !issue.valid?
120 assert !issue.valid?
121 assert_include 'Estimated time is invalid', issue.errors.full_messages
121 assert_include 'Estimated time is invalid', issue.errors.full_messages
122 end
122 end
123 end
123 end
124
124
125 def test_create_with_required_custom_field
125 def test_create_with_required_custom_field
126 set_language_if_valid 'en'
126 set_language_if_valid 'en'
127 field = IssueCustomField.find_by_name('Database')
127 field = IssueCustomField.find_by_name('Database')
128 field.update_attribute(:is_required, true)
128 field.update_attribute(:is_required, true)
129
129
130 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
130 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
131 :status_id => 1, :subject => 'test_create',
131 :status_id => 1, :subject => 'test_create',
132 :description => 'IssueTest#test_create_with_required_custom_field')
132 :description => 'IssueTest#test_create_with_required_custom_field')
133 assert issue.available_custom_fields.include?(field)
133 assert issue.available_custom_fields.include?(field)
134 # No value for the custom field
134 # No value for the custom field
135 assert !issue.save
135 assert !issue.save
136 assert_equal ["Database can't be blank"], issue.errors.full_messages
136 assert_equal ["Database can't be blank"], issue.errors.full_messages
137 # Blank value
137 # Blank value
138 issue.custom_field_values = { field.id => '' }
138 issue.custom_field_values = { field.id => '' }
139 assert !issue.save
139 assert !issue.save
140 assert_equal ["Database can't be blank"], issue.errors.full_messages
140 assert_equal ["Database can't be blank"], issue.errors.full_messages
141 # Invalid value
141 # Invalid value
142 issue.custom_field_values = { field.id => 'SQLServer' }
142 issue.custom_field_values = { field.id => 'SQLServer' }
143 assert !issue.save
143 assert !issue.save
144 assert_equal ["Database is not included in the list"], issue.errors.full_messages
144 assert_equal ["Database is not included in the list"], issue.errors.full_messages
145 # Valid value
145 # Valid value
146 issue.custom_field_values = { field.id => 'PostgreSQL' }
146 issue.custom_field_values = { field.id => 'PostgreSQL' }
147 assert issue.save
147 assert issue.save
148 issue.reload
148 issue.reload
149 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
149 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
150 end
150 end
151
151
152 def test_create_with_group_assignment
152 def test_create_with_group_assignment
153 with_settings :issue_group_assignment => '1' do
153 with_settings :issue_group_assignment => '1' do
154 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
154 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
155 :subject => 'Group assignment',
155 :subject => 'Group assignment',
156 :assigned_to_id => 11).save
156 :assigned_to_id => 11).save
157 issue = Issue.order('id DESC').first
157 issue = Issue.order('id DESC').first
158 assert_kind_of Group, issue.assigned_to
158 assert_kind_of Group, issue.assigned_to
159 assert_equal Group.find(11), issue.assigned_to
159 assert_equal Group.find(11), issue.assigned_to
160 end
160 end
161 end
161 end
162
162
163 def test_create_with_parent_issue_id
163 def test_create_with_parent_issue_id
164 issue = Issue.new(:project_id => 1, :tracker_id => 1,
164 issue = Issue.new(:project_id => 1, :tracker_id => 1,
165 :author_id => 1, :subject => 'Group assignment',
165 :author_id => 1, :subject => 'Group assignment',
166 :parent_issue_id => 1)
166 :parent_issue_id => 1)
167 assert_save issue
167 assert_save issue
168 assert_equal 1, issue.parent_issue_id
168 assert_equal 1, issue.parent_issue_id
169 assert_equal Issue.find(1), issue.parent
169 assert_equal Issue.find(1), issue.parent
170 end
170 end
171
171
172 def test_create_with_sharp_parent_issue_id
172 def test_create_with_sharp_parent_issue_id
173 issue = Issue.new(:project_id => 1, :tracker_id => 1,
173 issue = Issue.new(:project_id => 1, :tracker_id => 1,
174 :author_id => 1, :subject => 'Group assignment',
174 :author_id => 1, :subject => 'Group assignment',
175 :parent_issue_id => "#1")
175 :parent_issue_id => "#1")
176 assert_save issue
176 assert_save issue
177 assert_equal 1, issue.parent_issue_id
177 assert_equal 1, issue.parent_issue_id
178 assert_equal Issue.find(1), issue.parent
178 assert_equal Issue.find(1), issue.parent
179 end
179 end
180
180
181 def test_create_with_invalid_parent_issue_id
181 def test_create_with_invalid_parent_issue_id
182 set_language_if_valid 'en'
182 set_language_if_valid 'en'
183 issue = Issue.new(:project_id => 1, :tracker_id => 1,
183 issue = Issue.new(:project_id => 1, :tracker_id => 1,
184 :author_id => 1, :subject => 'Group assignment',
184 :author_id => 1, :subject => 'Group assignment',
185 :parent_issue_id => '01ABC')
185 :parent_issue_id => '01ABC')
186 assert !issue.save
186 assert !issue.save
187 assert_equal '01ABC', issue.parent_issue_id
187 assert_equal '01ABC', issue.parent_issue_id
188 assert_include 'Parent task is invalid', issue.errors.full_messages
188 assert_include 'Parent task is invalid', issue.errors.full_messages
189 end
189 end
190
190
191 def test_create_with_invalid_sharp_parent_issue_id
191 def test_create_with_invalid_sharp_parent_issue_id
192 set_language_if_valid 'en'
192 set_language_if_valid 'en'
193 issue = Issue.new(:project_id => 1, :tracker_id => 1,
193 issue = Issue.new(:project_id => 1, :tracker_id => 1,
194 :author_id => 1, :subject => 'Group assignment',
194 :author_id => 1, :subject => 'Group assignment',
195 :parent_issue_id => '#01ABC')
195 :parent_issue_id => '#01ABC')
196 assert !issue.save
196 assert !issue.save
197 assert_equal '#01ABC', issue.parent_issue_id
197 assert_equal '#01ABC', issue.parent_issue_id
198 assert_include 'Parent task is invalid', issue.errors.full_messages
198 assert_include 'Parent task is invalid', issue.errors.full_messages
199 end
199 end
200
200
201 def assert_visibility_match(user, issues)
201 def assert_visibility_match(user, issues)
202 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
202 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
203 end
203 end
204
204
205 def test_visible_scope_for_anonymous
205 def test_visible_scope_for_anonymous
206 # Anonymous user should see issues of public projects only
206 # Anonymous user should see issues of public projects only
207 issues = Issue.visible(User.anonymous).all
207 issues = Issue.visible(User.anonymous).all
208 assert issues.any?
208 assert issues.any?
209 assert_nil issues.detect {|issue| !issue.project.is_public?}
209 assert_nil issues.detect {|issue| !issue.project.is_public?}
210 assert_nil issues.detect {|issue| issue.is_private?}
210 assert_nil issues.detect {|issue| issue.is_private?}
211 assert_visibility_match User.anonymous, issues
211 assert_visibility_match User.anonymous, issues
212 end
212 end
213
213
214 def test_visible_scope_for_anonymous_without_view_issues_permissions
214 def test_visible_scope_for_anonymous_without_view_issues_permissions
215 # Anonymous user should not see issues without permission
215 # Anonymous user should not see issues without permission
216 Role.anonymous.remove_permission!(:view_issues)
216 Role.anonymous.remove_permission!(:view_issues)
217 issues = Issue.visible(User.anonymous).all
217 issues = Issue.visible(User.anonymous).all
218 assert issues.empty?
218 assert issues.empty?
219 assert_visibility_match User.anonymous, issues
219 assert_visibility_match User.anonymous, issues
220 end
220 end
221
221
222 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
222 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_default
223 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
223 assert Role.anonymous.update_attribute(:issues_visibility, 'default')
224 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
224 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
225 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
225 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
226 assert !issue.visible?(User.anonymous)
226 assert !issue.visible?(User.anonymous)
227 end
227 end
228
228
229 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
229 def test_anonymous_should_not_see_private_issues_with_issues_visibility_set_to_own
230 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
230 assert Role.anonymous.update_attribute(:issues_visibility, 'own')
231 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
231 issue = Issue.generate!(:author => User.anonymous, :assigned_to => User.anonymous, :is_private => true)
232 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
232 assert_nil Issue.where(:id => issue.id).visible(User.anonymous).first
233 assert !issue.visible?(User.anonymous)
233 assert !issue.visible?(User.anonymous)
234 end
234 end
235
235
236 def test_visible_scope_for_non_member
236 def test_visible_scope_for_non_member
237 user = User.find(9)
237 user = User.find(9)
238 assert user.projects.empty?
238 assert user.projects.empty?
239 # Non member user should see issues of public projects only
239 # Non member user should see issues of public projects only
240 issues = Issue.visible(user).all
240 issues = Issue.visible(user).all
241 assert issues.any?
241 assert issues.any?
242 assert_nil issues.detect {|issue| !issue.project.is_public?}
242 assert_nil issues.detect {|issue| !issue.project.is_public?}
243 assert_nil issues.detect {|issue| issue.is_private?}
243 assert_nil issues.detect {|issue| issue.is_private?}
244 assert_visibility_match user, issues
244 assert_visibility_match user, issues
245 end
245 end
246
246
247 def test_visible_scope_for_non_member_with_own_issues_visibility
247 def test_visible_scope_for_non_member_with_own_issues_visibility
248 Role.non_member.update_attribute :issues_visibility, 'own'
248 Role.non_member.update_attribute :issues_visibility, 'own'
249 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
249 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
250 user = User.find(9)
250 user = User.find(9)
251
251
252 issues = Issue.visible(user).all
252 issues = Issue.visible(user).all
253 assert issues.any?
253 assert issues.any?
254 assert_nil issues.detect {|issue| issue.author != user}
254 assert_nil issues.detect {|issue| issue.author != user}
255 assert_visibility_match user, issues
255 assert_visibility_match user, issues
256 end
256 end
257
257
258 def test_visible_scope_for_non_member_without_view_issues_permissions
258 def test_visible_scope_for_non_member_without_view_issues_permissions
259 # Non member user should not see issues without permission
259 # Non member user should not see issues without permission
260 Role.non_member.remove_permission!(:view_issues)
260 Role.non_member.remove_permission!(:view_issues)
261 user = User.find(9)
261 user = User.find(9)
262 assert user.projects.empty?
262 assert user.projects.empty?
263 issues = Issue.visible(user).all
263 issues = Issue.visible(user).all
264 assert issues.empty?
264 assert issues.empty?
265 assert_visibility_match user, issues
265 assert_visibility_match user, issues
266 end
266 end
267
267
268 def test_visible_scope_for_member
268 def test_visible_scope_for_member
269 user = User.find(9)
269 user = User.find(9)
270 # User should see issues of projects for which user has view_issues permissions only
270 # User should see issues of projects for which user has view_issues permissions only
271 Role.non_member.remove_permission!(:view_issues)
271 Role.non_member.remove_permission!(:view_issues)
272 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
272 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
273 issues = Issue.visible(user).all
273 issues = Issue.visible(user).all
274 assert issues.any?
274 assert issues.any?
275 assert_nil issues.detect {|issue| issue.project_id != 3}
275 assert_nil issues.detect {|issue| issue.project_id != 3}
276 assert_nil issues.detect {|issue| issue.is_private?}
276 assert_nil issues.detect {|issue| issue.is_private?}
277 assert_visibility_match user, issues
277 assert_visibility_match user, issues
278 end
278 end
279
279
280 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
280 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
281 user = User.find(8)
281 user = User.find(8)
282 assert user.groups.any?
282 assert user.groups.any?
283 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
283 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
284 Role.non_member.remove_permission!(:view_issues)
284 Role.non_member.remove_permission!(:view_issues)
285
285
286 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
286 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
287 :status_id => 1, :priority => IssuePriority.all.first,
287 :status_id => 1, :priority => IssuePriority.all.first,
288 :subject => 'Assignment test',
288 :subject => 'Assignment test',
289 :assigned_to => user.groups.first,
289 :assigned_to => user.groups.first,
290 :is_private => true)
290 :is_private => true)
291
291
292 Role.find(2).update_attribute :issues_visibility, 'default'
292 Role.find(2).update_attribute :issues_visibility, 'default'
293 issues = Issue.visible(User.find(8)).all
293 issues = Issue.visible(User.find(8)).all
294 assert issues.any?
294 assert issues.any?
295 assert issues.include?(issue)
295 assert issues.include?(issue)
296
296
297 Role.find(2).update_attribute :issues_visibility, 'own'
297 Role.find(2).update_attribute :issues_visibility, 'own'
298 issues = Issue.visible(User.find(8)).all
298 issues = Issue.visible(User.find(8)).all
299 assert issues.any?
299 assert issues.any?
300 assert issues.include?(issue)
300 assert issues.include?(issue)
301 end
301 end
302
302
303 def test_visible_scope_for_admin
303 def test_visible_scope_for_admin
304 user = User.find(1)
304 user = User.find(1)
305 user.members.each(&:destroy)
305 user.members.each(&:destroy)
306 assert user.projects.empty?
306 assert user.projects.empty?
307 issues = Issue.visible(user).all
307 issues = Issue.visible(user).all
308 assert issues.any?
308 assert issues.any?
309 # Admin should see issues on private projects that admin does not belong to
309 # Admin should see issues on private projects that admin does not belong to
310 assert issues.detect {|issue| !issue.project.is_public?}
310 assert issues.detect {|issue| !issue.project.is_public?}
311 # Admin should see private issues of other users
311 # Admin should see private issues of other users
312 assert issues.detect {|issue| issue.is_private? && issue.author != user}
312 assert issues.detect {|issue| issue.is_private? && issue.author != user}
313 assert_visibility_match user, issues
313 assert_visibility_match user, issues
314 end
314 end
315
315
316 def test_visible_scope_with_project
316 def test_visible_scope_with_project
317 project = Project.find(1)
317 project = Project.find(1)
318 issues = Issue.visible(User.find(2), :project => project).all
318 issues = Issue.visible(User.find(2), :project => project).all
319 projects = issues.collect(&:project).uniq
319 projects = issues.collect(&:project).uniq
320 assert_equal 1, projects.size
320 assert_equal 1, projects.size
321 assert_equal project, projects.first
321 assert_equal project, projects.first
322 end
322 end
323
323
324 def test_visible_scope_with_project_and_subprojects
324 def test_visible_scope_with_project_and_subprojects
325 project = Project.find(1)
325 project = Project.find(1)
326 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
326 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
327 projects = issues.collect(&:project).uniq
327 projects = issues.collect(&:project).uniq
328 assert projects.size > 1
328 assert projects.size > 1
329 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
329 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
330 end
330 end
331
331
332 def test_visible_and_nested_set_scopes
332 def test_visible_and_nested_set_scopes
333 user = User.generate!
333 user = User.generate!
334 parent = Issue.generate!(:assigned_to => user)
334 parent = Issue.generate!(:assigned_to => user)
335 assert parent.visible?(user)
335 assert parent.visible?(user)
336 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
336 child1 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
337 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
337 child2 = Issue.generate!(:parent_issue_id => parent.id, :assigned_to => user)
338 parent.reload
338 parent.reload
339 child1.reload
339 child1.reload
340 child2.reload
340 child2.reload
341 assert child1.visible?(user)
341 assert child1.visible?(user)
342 assert child2.visible?(user)
342 assert child2.visible?(user)
343 assert_equal 2, parent.descendants.count
343 assert_equal 2, parent.descendants.count
344 assert_equal 2, parent.descendants.visible(user).count
344 assert_equal 2, parent.descendants.visible(user).count
345 # awesome_nested_set 2-1-stable branch has regression.
345 # awesome_nested_set 2-1-stable branch has regression.
346 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
346 # https://github.com/collectiveidea/awesome_nested_set/commit/3d5ac746542b564f6586c2316180254b088bebb6
347 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
347 # ActiveRecord::StatementInvalid: SQLite3::SQLException: ambiguous column name: lft:
348 assert_equal 2, parent.descendants.collect{|i| i}.size
348 assert_equal 2, parent.descendants.collect{|i| i}.size
349 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
349 assert_equal 2, parent.descendants.visible(user).collect{|i| i}.size
350 end
350 end
351
351
352 def test_open_scope
352 def test_open_scope
353 issues = Issue.open.all
353 issues = Issue.open.all
354 assert_nil issues.detect(&:closed?)
354 assert_nil issues.detect(&:closed?)
355 end
355 end
356
356
357 def test_open_scope_with_arg
357 def test_open_scope_with_arg
358 issues = Issue.open(false).all
358 issues = Issue.open(false).all
359 assert_equal issues, issues.select(&:closed?)
359 assert_equal issues, issues.select(&:closed?)
360 end
360 end
361
361
362 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
362 def test_fixed_version_scope_with_a_version_should_return_its_fixed_issues
363 version = Version.find(2)
363 version = Version.find(2)
364 assert version.fixed_issues.any?
364 assert version.fixed_issues.any?
365 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
365 assert_equal version.fixed_issues.to_a.sort, Issue.fixed_version(version).to_a.sort
366 end
366 end
367
367
368 def test_fixed_version_scope_with_empty_array_should_return_no_result
368 def test_fixed_version_scope_with_empty_array_should_return_no_result
369 assert_equal 0, Issue.fixed_version([]).count
369 assert_equal 0, Issue.fixed_version([]).count
370 end
370 end
371
371
372 def test_errors_full_messages_should_include_custom_fields_errors
372 def test_errors_full_messages_should_include_custom_fields_errors
373 field = IssueCustomField.find_by_name('Database')
373 field = IssueCustomField.find_by_name('Database')
374
374
375 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
375 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
376 :status_id => 1, :subject => 'test_create',
376 :status_id => 1, :subject => 'test_create',
377 :description => 'IssueTest#test_create_with_required_custom_field')
377 :description => 'IssueTest#test_create_with_required_custom_field')
378 assert issue.available_custom_fields.include?(field)
378 assert issue.available_custom_fields.include?(field)
379 # Invalid value
379 # Invalid value
380 issue.custom_field_values = { field.id => 'SQLServer' }
380 issue.custom_field_values = { field.id => 'SQLServer' }
381
381
382 assert !issue.valid?
382 assert !issue.valid?
383 assert_equal 1, issue.errors.full_messages.size
383 assert_equal 1, issue.errors.full_messages.size
384 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
384 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
385 issue.errors.full_messages.first
385 issue.errors.full_messages.first
386 end
386 end
387
387
388 def test_update_issue_with_required_custom_field
388 def test_update_issue_with_required_custom_field
389 field = IssueCustomField.find_by_name('Database')
389 field = IssueCustomField.find_by_name('Database')
390 field.update_attribute(:is_required, true)
390 field.update_attribute(:is_required, true)
391
391
392 issue = Issue.find(1)
392 issue = Issue.find(1)
393 assert_nil issue.custom_value_for(field)
393 assert_nil issue.custom_value_for(field)
394 assert issue.available_custom_fields.include?(field)
394 assert issue.available_custom_fields.include?(field)
395 # No change to custom values, issue can be saved
395 # No change to custom values, issue can be saved
396 assert issue.save
396 assert issue.save
397 # Blank value
397 # Blank value
398 issue.custom_field_values = { field.id => '' }
398 issue.custom_field_values = { field.id => '' }
399 assert !issue.save
399 assert !issue.save
400 # Valid value
400 # Valid value
401 issue.custom_field_values = { field.id => 'PostgreSQL' }
401 issue.custom_field_values = { field.id => 'PostgreSQL' }
402 assert issue.save
402 assert issue.save
403 issue.reload
403 issue.reload
404 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
404 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
405 end
405 end
406
406
407 def test_should_not_update_attributes_if_custom_fields_validation_fails
407 def test_should_not_update_attributes_if_custom_fields_validation_fails
408 issue = Issue.find(1)
408 issue = Issue.find(1)
409 field = IssueCustomField.find_by_name('Database')
409 field = IssueCustomField.find_by_name('Database')
410 assert issue.available_custom_fields.include?(field)
410 assert issue.available_custom_fields.include?(field)
411
411
412 issue.custom_field_values = { field.id => 'Invalid' }
412 issue.custom_field_values = { field.id => 'Invalid' }
413 issue.subject = 'Should be not be saved'
413 issue.subject = 'Should be not be saved'
414 assert !issue.save
414 assert !issue.save
415
415
416 issue.reload
416 issue.reload
417 assert_equal "Can't print recipes", issue.subject
417 assert_equal "Can't print recipes", issue.subject
418 end
418 end
419
419
420 def test_should_not_recreate_custom_values_objects_on_update
420 def test_should_not_recreate_custom_values_objects_on_update
421 field = IssueCustomField.find_by_name('Database')
421 field = IssueCustomField.find_by_name('Database')
422
422
423 issue = Issue.find(1)
423 issue = Issue.find(1)
424 issue.custom_field_values = { field.id => 'PostgreSQL' }
424 issue.custom_field_values = { field.id => 'PostgreSQL' }
425 assert issue.save
425 assert issue.save
426 custom_value = issue.custom_value_for(field)
426 custom_value = issue.custom_value_for(field)
427 issue.reload
427 issue.reload
428 issue.custom_field_values = { field.id => 'MySQL' }
428 issue.custom_field_values = { field.id => 'MySQL' }
429 assert issue.save
429 assert issue.save
430 issue.reload
430 issue.reload
431 assert_equal custom_value.id, issue.custom_value_for(field).id
431 assert_equal custom_value.id, issue.custom_value_for(field).id
432 end
432 end
433
433
434 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
434 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
435 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
435 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1,
436 :status_id => 1, :subject => 'Test',
436 :status_id => 1, :subject => 'Test',
437 :custom_field_values => {'2' => 'Test'})
437 :custom_field_values => {'2' => 'Test'})
438 assert !Tracker.find(2).custom_field_ids.include?(2)
438 assert !Tracker.find(2).custom_field_ids.include?(2)
439
439
440 issue = Issue.find(issue.id)
440 issue = Issue.find(issue.id)
441 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
441 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
442
442
443 issue = Issue.find(issue.id)
443 issue = Issue.find(issue.id)
444 custom_value = issue.custom_value_for(2)
444 custom_value = issue.custom_value_for(2)
445 assert_not_nil custom_value
445 assert_not_nil custom_value
446 assert_equal 'Test', custom_value.value
446 assert_equal 'Test', custom_value.value
447 end
447 end
448
448
449 def test_assigning_tracker_id_should_reload_custom_fields_values
449 def test_assigning_tracker_id_should_reload_custom_fields_values
450 issue = Issue.new(:project => Project.find(1))
450 issue = Issue.new(:project => Project.find(1))
451 assert issue.custom_field_values.empty?
451 assert issue.custom_field_values.empty?
452 issue.tracker_id = 1
452 issue.tracker_id = 1
453 assert issue.custom_field_values.any?
453 assert issue.custom_field_values.any?
454 end
454 end
455
455
456 def test_assigning_attributes_should_assign_project_and_tracker_first
456 def test_assigning_attributes_should_assign_project_and_tracker_first
457 seq = sequence('seq')
457 seq = sequence('seq')
458 issue = Issue.new
458 issue = Issue.new
459 issue.expects(:project_id=).in_sequence(seq)
459 issue.expects(:project_id=).in_sequence(seq)
460 issue.expects(:tracker_id=).in_sequence(seq)
460 issue.expects(:tracker_id=).in_sequence(seq)
461 issue.expects(:subject=).in_sequence(seq)
461 issue.expects(:subject=).in_sequence(seq)
462 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
462 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
463 end
463 end
464
464
465 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
465 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
466 attributes = ActiveSupport::OrderedHash.new
466 attributes = ActiveSupport::OrderedHash.new
467 attributes['custom_field_values'] = { '1' => 'MySQL' }
467 attributes['custom_field_values'] = { '1' => 'MySQL' }
468 attributes['tracker_id'] = '1'
468 attributes['tracker_id'] = '1'
469 issue = Issue.new(:project => Project.find(1))
469 issue = Issue.new(:project => Project.find(1))
470 issue.attributes = attributes
470 issue.attributes = attributes
471 assert_equal 'MySQL', issue.custom_field_value(1)
471 assert_equal 'MySQL', issue.custom_field_value(1)
472 end
472 end
473
473
474 def test_reload_should_reload_custom_field_values
474 def test_reload_should_reload_custom_field_values
475 issue = Issue.generate!
475 issue = Issue.generate!
476 issue.custom_field_values = {'2' => 'Foo'}
476 issue.custom_field_values = {'2' => 'Foo'}
477 issue.save!
477 issue.save!
478
478
479 issue = Issue.order('id desc').first
479 issue = Issue.order('id desc').first
480 assert_equal 'Foo', issue.custom_field_value(2)
480 assert_equal 'Foo', issue.custom_field_value(2)
481
481
482 issue.custom_field_values = {'2' => 'Bar'}
482 issue.custom_field_values = {'2' => 'Bar'}
483 assert_equal 'Bar', issue.custom_field_value(2)
483 assert_equal 'Bar', issue.custom_field_value(2)
484
484
485 issue.reload
485 issue.reload
486 assert_equal 'Foo', issue.custom_field_value(2)
486 assert_equal 'Foo', issue.custom_field_value(2)
487 end
487 end
488
488
489 def test_should_update_issue_with_disabled_tracker
489 def test_should_update_issue_with_disabled_tracker
490 p = Project.find(1)
490 p = Project.find(1)
491 issue = Issue.find(1)
491 issue = Issue.find(1)
492
492
493 p.trackers.delete(issue.tracker)
493 p.trackers.delete(issue.tracker)
494 assert !p.trackers.include?(issue.tracker)
494 assert !p.trackers.include?(issue.tracker)
495
495
496 issue.reload
496 issue.reload
497 issue.subject = 'New subject'
497 issue.subject = 'New subject'
498 assert issue.save
498 assert issue.save
499 end
499 end
500
500
501 def test_should_not_set_a_disabled_tracker
501 def test_should_not_set_a_disabled_tracker
502 p = Project.find(1)
502 p = Project.find(1)
503 p.trackers.delete(Tracker.find(2))
503 p.trackers.delete(Tracker.find(2))
504
504
505 issue = Issue.find(1)
505 issue = Issue.find(1)
506 issue.tracker_id = 2
506 issue.tracker_id = 2
507 issue.subject = 'New subject'
507 issue.subject = 'New subject'
508 assert !issue.save
508 assert !issue.save
509 assert_not_equal [], issue.errors[:tracker_id]
509 assert_not_equal [], issue.errors[:tracker_id]
510 end
510 end
511
511
512 def test_category_based_assignment
512 def test_category_based_assignment
513 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
513 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
514 :status_id => 1, :priority => IssuePriority.all.first,
514 :status_id => 1, :priority => IssuePriority.all.first,
515 :subject => 'Assignment test',
515 :subject => 'Assignment test',
516 :description => 'Assignment test', :category_id => 1)
516 :description => 'Assignment test', :category_id => 1)
517 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
517 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
518 end
518 end
519
519
520 def test_new_statuses_allowed_to
520 def test_new_statuses_allowed_to
521 WorkflowTransition.delete_all
521 WorkflowTransition.delete_all
522 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
522 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
523 :old_status_id => 1, :new_status_id => 2,
523 :old_status_id => 1, :new_status_id => 2,
524 :author => false, :assignee => false)
524 :author => false, :assignee => false)
525 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
525 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
526 :old_status_id => 1, :new_status_id => 3,
526 :old_status_id => 1, :new_status_id => 3,
527 :author => true, :assignee => false)
527 :author => true, :assignee => false)
528 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
528 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
529 :old_status_id => 1, :new_status_id => 4,
529 :old_status_id => 1, :new_status_id => 4,
530 :author => false, :assignee => true)
530 :author => false, :assignee => true)
531 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
531 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
532 :old_status_id => 1, :new_status_id => 5,
532 :old_status_id => 1, :new_status_id => 5,
533 :author => true, :assignee => true)
533 :author => true, :assignee => true)
534 status = IssueStatus.find(1)
534 status = IssueStatus.find(1)
535 role = Role.find(1)
535 role = Role.find(1)
536 tracker = Tracker.find(1)
536 tracker = Tracker.find(1)
537 user = User.find(2)
537 user = User.find(2)
538
538
539 issue = Issue.generate!(:tracker => tracker, :status => status,
539 issue = Issue.generate!(:tracker => tracker, :status => status,
540 :project_id => 1, :author_id => 1)
540 :project_id => 1, :author_id => 1)
541 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
541 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
542
542
543 issue = Issue.generate!(:tracker => tracker, :status => status,
543 issue = Issue.generate!(:tracker => tracker, :status => status,
544 :project_id => 1, :author => user)
544 :project_id => 1, :author => user)
545 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
545 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
546
546
547 issue = Issue.generate!(:tracker => tracker, :status => status,
547 issue = Issue.generate!(:tracker => tracker, :status => status,
548 :project_id => 1, :author_id => 1,
548 :project_id => 1, :author_id => 1,
549 :assigned_to => user)
549 :assigned_to => user)
550 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
550 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
551
551
552 issue = Issue.generate!(:tracker => tracker, :status => status,
552 issue = Issue.generate!(:tracker => tracker, :status => status,
553 :project_id => 1, :author => user,
553 :project_id => 1, :author => user,
554 :assigned_to => user)
554 :assigned_to => user)
555 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
555 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
556
556
557 group = Group.generate!
557 group = Group.generate!
558 group.users << user
558 group.users << user
559 issue = Issue.generate!(:tracker => tracker, :status => status,
559 issue = Issue.generate!(:tracker => tracker, :status => status,
560 :project_id => 1, :author => user,
560 :project_id => 1, :author => user,
561 :assigned_to => group)
561 :assigned_to => group)
562 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
562 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
563 end
563 end
564
564
565 def test_new_statuses_allowed_to_should_consider_group_assignment
565 def test_new_statuses_allowed_to_should_consider_group_assignment
566 WorkflowTransition.delete_all
566 WorkflowTransition.delete_all
567 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
567 WorkflowTransition.create!(:role_id => 1, :tracker_id => 1,
568 :old_status_id => 1, :new_status_id => 4,
568 :old_status_id => 1, :new_status_id => 4,
569 :author => false, :assignee => true)
569 :author => false, :assignee => true)
570 user = User.find(2)
570 user = User.find(2)
571 group = Group.generate!
571 group = Group.generate!
572 group.users << user
572 group.users << user
573
573
574 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
574 issue = Issue.generate!(:author_id => 1, :assigned_to => group)
575 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
575 assert_include 4, issue.new_statuses_allowed_to(user).map(&:id)
576 end
576 end
577
577
578 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
578 def test_new_statuses_allowed_to_should_return_all_transitions_for_admin
579 admin = User.find(1)
579 admin = User.find(1)
580 issue = Issue.find(1)
580 issue = Issue.find(1)
581 assert !admin.member_of?(issue.project)
581 assert !admin.member_of?(issue.project)
582 expected_statuses = [issue.status] +
582 expected_statuses = [issue.status] +
583 WorkflowTransition.where(:old_status_id => issue.status_id).
583 WorkflowTransition.where(:old_status_id => issue.status_id).
584 map(&:new_status).uniq.sort
584 map(&:new_status).uniq.sort
585 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
585 assert_equal expected_statuses, issue.new_statuses_allowed_to(admin)
586 end
586 end
587
587
588 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
588 def test_new_statuses_allowed_to_should_return_default_and_current_status_when_copying
589 issue = Issue.find(1).copy
589 issue = Issue.find(1).copy
590 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
590 assert_equal [1], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
591
591
592 issue = Issue.find(2).copy
592 issue = Issue.find(2).copy
593 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
593 assert_equal [1, 2], issue.new_statuses_allowed_to(User.find(2)).map(&:id)
594 end
594 end
595
595
596 def test_safe_attributes_names_should_not_include_disabled_field
596 def test_safe_attributes_names_should_not_include_disabled_field
597 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
597 tracker = Tracker.new(:core_fields => %w(assigned_to_id fixed_version_id))
598
598
599 issue = Issue.new(:tracker => tracker)
599 issue = Issue.new(:tracker => tracker)
600 assert_include 'tracker_id', issue.safe_attribute_names
600 assert_include 'tracker_id', issue.safe_attribute_names
601 assert_include 'status_id', issue.safe_attribute_names
601 assert_include 'status_id', issue.safe_attribute_names
602 assert_include 'subject', issue.safe_attribute_names
602 assert_include 'subject', issue.safe_attribute_names
603 assert_include 'description', issue.safe_attribute_names
603 assert_include 'description', issue.safe_attribute_names
604 assert_include 'custom_field_values', issue.safe_attribute_names
604 assert_include 'custom_field_values', issue.safe_attribute_names
605 assert_include 'custom_fields', issue.safe_attribute_names
605 assert_include 'custom_fields', issue.safe_attribute_names
606 assert_include 'lock_version', issue.safe_attribute_names
606 assert_include 'lock_version', issue.safe_attribute_names
607
607
608 tracker.core_fields.each do |field|
608 tracker.core_fields.each do |field|
609 assert_include field, issue.safe_attribute_names
609 assert_include field, issue.safe_attribute_names
610 end
610 end
611
611
612 tracker.disabled_core_fields.each do |field|
612 tracker.disabled_core_fields.each do |field|
613 assert_not_include field, issue.safe_attribute_names
613 assert_not_include field, issue.safe_attribute_names
614 end
614 end
615 end
615 end
616
616
617 def test_safe_attributes_should_ignore_disabled_fields
617 def test_safe_attributes_should_ignore_disabled_fields
618 tracker = Tracker.find(1)
618 tracker = Tracker.find(1)
619 tracker.core_fields = %w(assigned_to_id due_date)
619 tracker.core_fields = %w(assigned_to_id due_date)
620 tracker.save!
620 tracker.save!
621
621
622 issue = Issue.new(:tracker => tracker)
622 issue = Issue.new(:tracker => tracker)
623 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
623 issue.safe_attributes = {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}
624 assert_nil issue.start_date
624 assert_nil issue.start_date
625 assert_equal Date.parse('2012-07-14'), issue.due_date
625 assert_equal Date.parse('2012-07-14'), issue.due_date
626 end
626 end
627
627
628 def test_safe_attributes_should_accept_target_tracker_enabled_fields
628 def test_safe_attributes_should_accept_target_tracker_enabled_fields
629 source = Tracker.find(1)
629 source = Tracker.find(1)
630 source.core_fields = []
630 source.core_fields = []
631 source.save!
631 source.save!
632 target = Tracker.find(2)
632 target = Tracker.find(2)
633 target.core_fields = %w(assigned_to_id due_date)
633 target.core_fields = %w(assigned_to_id due_date)
634 target.save!
634 target.save!
635
635
636 issue = Issue.new(:tracker => source)
636 issue = Issue.new(:tracker => source)
637 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
637 issue.safe_attributes = {'tracker_id' => 2, 'due_date' => '2012-07-14'}
638 assert_equal target, issue.tracker
638 assert_equal target, issue.tracker
639 assert_equal Date.parse('2012-07-14'), issue.due_date
639 assert_equal Date.parse('2012-07-14'), issue.due_date
640 end
640 end
641
641
642 def test_safe_attributes_should_not_include_readonly_fields
642 def test_safe_attributes_should_not_include_readonly_fields
643 WorkflowPermission.delete_all
643 WorkflowPermission.delete_all
644 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
644 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
645 :role_id => 1, :field_name => 'due_date',
645 :role_id => 1, :field_name => 'due_date',
646 :rule => 'readonly')
646 :rule => 'readonly')
647 user = User.find(2)
647 user = User.find(2)
648
648
649 issue = Issue.new(:project_id => 1, :tracker_id => 1)
649 issue = Issue.new(:project_id => 1, :tracker_id => 1)
650 assert_equal %w(due_date), issue.read_only_attribute_names(user)
650 assert_equal %w(due_date), issue.read_only_attribute_names(user)
651 assert_not_include 'due_date', issue.safe_attribute_names(user)
651 assert_not_include 'due_date', issue.safe_attribute_names(user)
652
652
653 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
653 issue.send :safe_attributes=, {'start_date' => '2012-07-14', 'due_date' => '2012-07-14'}, user
654 assert_equal Date.parse('2012-07-14'), issue.start_date
654 assert_equal Date.parse('2012-07-14'), issue.start_date
655 assert_nil issue.due_date
655 assert_nil issue.due_date
656 end
656 end
657
657
658 def test_safe_attributes_should_not_include_readonly_custom_fields
658 def test_safe_attributes_should_not_include_readonly_custom_fields
659 cf1 = IssueCustomField.create!(:name => 'Writable field',
659 cf1 = IssueCustomField.create!(:name => 'Writable field',
660 :field_format => 'string',
660 :field_format => 'string',
661 :is_for_all => true, :tracker_ids => [1])
661 :is_for_all => true, :tracker_ids => [1])
662 cf2 = IssueCustomField.create!(:name => 'Readonly field',
662 cf2 = IssueCustomField.create!(:name => 'Readonly field',
663 :field_format => 'string',
663 :field_format => 'string',
664 :is_for_all => true, :tracker_ids => [1])
664 :is_for_all => true, :tracker_ids => [1])
665 WorkflowPermission.delete_all
665 WorkflowPermission.delete_all
666 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
666 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
667 :role_id => 1, :field_name => cf2.id.to_s,
667 :role_id => 1, :field_name => cf2.id.to_s,
668 :rule => 'readonly')
668 :rule => 'readonly')
669 user = User.find(2)
669 user = User.find(2)
670 issue = Issue.new(:project_id => 1, :tracker_id => 1)
670 issue = Issue.new(:project_id => 1, :tracker_id => 1)
671 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
671 assert_equal [cf2.id.to_s], issue.read_only_attribute_names(user)
672 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
672 assert_not_include cf2.id.to_s, issue.safe_attribute_names(user)
673
673
674 issue.send :safe_attributes=, {'custom_field_values' => {
674 issue.send :safe_attributes=, {'custom_field_values' => {
675 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
675 cf1.id.to_s => 'value1', cf2.id.to_s => 'value2'
676 }}, user
676 }}, user
677 assert_equal 'value1', issue.custom_field_value(cf1)
677 assert_equal 'value1', issue.custom_field_value(cf1)
678 assert_nil issue.custom_field_value(cf2)
678 assert_nil issue.custom_field_value(cf2)
679
679
680 issue.send :safe_attributes=, {'custom_fields' => [
680 issue.send :safe_attributes=, {'custom_fields' => [
681 {'id' => cf1.id.to_s, 'value' => 'valuea'},
681 {'id' => cf1.id.to_s, 'value' => 'valuea'},
682 {'id' => cf2.id.to_s, 'value' => 'valueb'}
682 {'id' => cf2.id.to_s, 'value' => 'valueb'}
683 ]}, user
683 ]}, user
684 assert_equal 'valuea', issue.custom_field_value(cf1)
684 assert_equal 'valuea', issue.custom_field_value(cf1)
685 assert_nil issue.custom_field_value(cf2)
685 assert_nil issue.custom_field_value(cf2)
686 end
686 end
687
687
688 def test_editable_custom_field_values_should_return_non_readonly_custom_values
688 def test_editable_custom_field_values_should_return_non_readonly_custom_values
689 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
689 cf1 = IssueCustomField.create!(:name => 'Writable field', :field_format => 'string',
690 :is_for_all => true, :tracker_ids => [1, 2])
690 :is_for_all => true, :tracker_ids => [1, 2])
691 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
691 cf2 = IssueCustomField.create!(:name => 'Readonly field', :field_format => 'string',
692 :is_for_all => true, :tracker_ids => [1, 2])
692 :is_for_all => true, :tracker_ids => [1, 2])
693 WorkflowPermission.delete_all
693 WorkflowPermission.delete_all
694 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
694 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1, :role_id => 1,
695 :field_name => cf2.id.to_s, :rule => 'readonly')
695 :field_name => cf2.id.to_s, :rule => 'readonly')
696 user = User.find(2)
696 user = User.find(2)
697
697
698 issue = Issue.new(:project_id => 1, :tracker_id => 1)
698 issue = Issue.new(:project_id => 1, :tracker_id => 1)
699 values = issue.editable_custom_field_values(user)
699 values = issue.editable_custom_field_values(user)
700 assert values.detect {|value| value.custom_field == cf1}
700 assert values.detect {|value| value.custom_field == cf1}
701 assert_nil values.detect {|value| value.custom_field == cf2}
701 assert_nil values.detect {|value| value.custom_field == cf2}
702
702
703 issue.tracker_id = 2
703 issue.tracker_id = 2
704 values = issue.editable_custom_field_values(user)
704 values = issue.editable_custom_field_values(user)
705 assert values.detect {|value| value.custom_field == cf1}
705 assert values.detect {|value| value.custom_field == cf1}
706 assert values.detect {|value| value.custom_field == cf2}
706 assert values.detect {|value| value.custom_field == cf2}
707 end
707 end
708
708
709 def test_editable_custom_fields_should_return_custom_field_that_is_enabled_for_the_role_only
710 enabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [1,2])
711 disabled_cf = IssueCustomField.generate!(:is_for_all => true, :tracker_ids => [1], :visible => false, :role_ids => [2])
712 user = User.find(2)
713 issue = Issue.new(:project_id => 1, :tracker_id => 1)
714
715 assert_include enabled_cf, issue.editable_custom_fields(user)
716 assert_not_include disabled_cf, issue.editable_custom_fields(user)
717 end
718
709 def test_safe_attributes_should_accept_target_tracker_writable_fields
719 def test_safe_attributes_should_accept_target_tracker_writable_fields
710 WorkflowPermission.delete_all
720 WorkflowPermission.delete_all
711 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
721 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
712 :role_id => 1, :field_name => 'due_date',
722 :role_id => 1, :field_name => 'due_date',
713 :rule => 'readonly')
723 :rule => 'readonly')
714 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
724 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
715 :role_id => 1, :field_name => 'start_date',
725 :role_id => 1, :field_name => 'start_date',
716 :rule => 'readonly')
726 :rule => 'readonly')
717 user = User.find(2)
727 user = User.find(2)
718
728
719 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
729 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
720
730
721 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
731 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
722 'due_date' => '2012-07-14'}, user
732 'due_date' => '2012-07-14'}, user
723 assert_equal Date.parse('2012-07-12'), issue.start_date
733 assert_equal Date.parse('2012-07-12'), issue.start_date
724 assert_nil issue.due_date
734 assert_nil issue.due_date
725
735
726 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
736 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
727 'due_date' => '2012-07-16',
737 'due_date' => '2012-07-16',
728 'tracker_id' => 2}, user
738 'tracker_id' => 2}, user
729 assert_equal Date.parse('2012-07-12'), issue.start_date
739 assert_equal Date.parse('2012-07-12'), issue.start_date
730 assert_equal Date.parse('2012-07-16'), issue.due_date
740 assert_equal Date.parse('2012-07-16'), issue.due_date
731 end
741 end
732
742
733 def test_safe_attributes_should_accept_target_status_writable_fields
743 def test_safe_attributes_should_accept_target_status_writable_fields
734 WorkflowPermission.delete_all
744 WorkflowPermission.delete_all
735 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
745 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
736 :role_id => 1, :field_name => 'due_date',
746 :role_id => 1, :field_name => 'due_date',
737 :rule => 'readonly')
747 :rule => 'readonly')
738 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
748 WorkflowPermission.create!(:old_status_id => 2, :tracker_id => 1,
739 :role_id => 1, :field_name => 'start_date',
749 :role_id => 1, :field_name => 'start_date',
740 :rule => 'readonly')
750 :rule => 'readonly')
741 user = User.find(2)
751 user = User.find(2)
742
752
743 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
753 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
744
754
745 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
755 issue.send :safe_attributes=, {'start_date' => '2012-07-12',
746 'due_date' => '2012-07-14'},
756 'due_date' => '2012-07-14'},
747 user
757 user
748 assert_equal Date.parse('2012-07-12'), issue.start_date
758 assert_equal Date.parse('2012-07-12'), issue.start_date
749 assert_nil issue.due_date
759 assert_nil issue.due_date
750
760
751 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
761 issue.send :safe_attributes=, {'start_date' => '2012-07-15',
752 'due_date' => '2012-07-16',
762 'due_date' => '2012-07-16',
753 'status_id' => 2},
763 'status_id' => 2},
754 user
764 user
755 assert_equal Date.parse('2012-07-12'), issue.start_date
765 assert_equal Date.parse('2012-07-12'), issue.start_date
756 assert_equal Date.parse('2012-07-16'), issue.due_date
766 assert_equal Date.parse('2012-07-16'), issue.due_date
757 end
767 end
758
768
759 def test_required_attributes_should_be_validated
769 def test_required_attributes_should_be_validated
760 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
770 cf = IssueCustomField.create!(:name => 'Foo', :field_format => 'string',
761 :is_for_all => true, :tracker_ids => [1, 2])
771 :is_for_all => true, :tracker_ids => [1, 2])
762
772
763 WorkflowPermission.delete_all
773 WorkflowPermission.delete_all
764 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
774 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
765 :role_id => 1, :field_name => 'due_date',
775 :role_id => 1, :field_name => 'due_date',
766 :rule => 'required')
776 :rule => 'required')
767 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
777 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
768 :role_id => 1, :field_name => 'category_id',
778 :role_id => 1, :field_name => 'category_id',
769 :rule => 'required')
779 :rule => 'required')
770 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
780 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
771 :role_id => 1, :field_name => cf.id.to_s,
781 :role_id => 1, :field_name => cf.id.to_s,
772 :rule => 'required')
782 :rule => 'required')
773
783
774 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
784 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
775 :role_id => 1, :field_name => 'start_date',
785 :role_id => 1, :field_name => 'start_date',
776 :rule => 'required')
786 :rule => 'required')
777 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
787 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 2,
778 :role_id => 1, :field_name => cf.id.to_s,
788 :role_id => 1, :field_name => cf.id.to_s,
779 :rule => 'required')
789 :rule => 'required')
780 user = User.find(2)
790 user = User.find(2)
781
791
782 issue = Issue.new(:project_id => 1, :tracker_id => 1,
792 issue = Issue.new(:project_id => 1, :tracker_id => 1,
783 :status_id => 1, :subject => 'Required fields',
793 :status_id => 1, :subject => 'Required fields',
784 :author => user)
794 :author => user)
785 assert_equal [cf.id.to_s, "category_id", "due_date"],
795 assert_equal [cf.id.to_s, "category_id", "due_date"],
786 issue.required_attribute_names(user).sort
796 issue.required_attribute_names(user).sort
787 assert !issue.save, "Issue was saved"
797 assert !issue.save, "Issue was saved"
788 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
798 assert_equal ["Category can't be blank", "Due date can't be blank", "Foo can't be blank"],
789 issue.errors.full_messages.sort
799 issue.errors.full_messages.sort
790
800
791 issue.tracker_id = 2
801 issue.tracker_id = 2
792 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
802 assert_equal [cf.id.to_s, "start_date"], issue.required_attribute_names(user).sort
793 assert !issue.save, "Issue was saved"
803 assert !issue.save, "Issue was saved"
794 assert_equal ["Foo can't be blank", "Start date can't be blank"],
804 assert_equal ["Foo can't be blank", "Start date can't be blank"],
795 issue.errors.full_messages.sort
805 issue.errors.full_messages.sort
796
806
797 issue.start_date = Date.today
807 issue.start_date = Date.today
798 issue.custom_field_values = {cf.id.to_s => 'bar'}
808 issue.custom_field_values = {cf.id.to_s => 'bar'}
799 assert issue.save
809 assert issue.save
800 end
810 end
801
811
802 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
812 def test_required_attribute_names_for_multiple_roles_should_intersect_rules
803 WorkflowPermission.delete_all
813 WorkflowPermission.delete_all
804 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
814 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
805 :role_id => 1, :field_name => 'due_date',
815 :role_id => 1, :field_name => 'due_date',
806 :rule => 'required')
816 :rule => 'required')
807 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
817 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
808 :role_id => 1, :field_name => 'start_date',
818 :role_id => 1, :field_name => 'start_date',
809 :rule => 'required')
819 :rule => 'required')
810 user = User.find(2)
820 user = User.find(2)
811 member = Member.find(1)
821 member = Member.find(1)
812 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
822 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
813
823
814 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
824 assert_equal %w(due_date start_date), issue.required_attribute_names(user).sort
815
825
816 member.role_ids = [1, 2]
826 member.role_ids = [1, 2]
817 member.save!
827 member.save!
818 assert_equal [], issue.required_attribute_names(user.reload)
828 assert_equal [], issue.required_attribute_names(user.reload)
819
829
820 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
830 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
821 :role_id => 2, :field_name => 'due_date',
831 :role_id => 2, :field_name => 'due_date',
822 :rule => 'required')
832 :rule => 'required')
823 assert_equal %w(due_date), issue.required_attribute_names(user)
833 assert_equal %w(due_date), issue.required_attribute_names(user)
824
834
825 member.role_ids = [1, 2, 3]
835 member.role_ids = [1, 2, 3]
826 member.save!
836 member.save!
827 assert_equal [], issue.required_attribute_names(user.reload)
837 assert_equal [], issue.required_attribute_names(user.reload)
828
838
829 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
839 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
830 :role_id => 2, :field_name => 'due_date',
840 :role_id => 2, :field_name => 'due_date',
831 :rule => 'readonly')
841 :rule => 'readonly')
832 # required + readonly => required
842 # required + readonly => required
833 assert_equal %w(due_date), issue.required_attribute_names(user)
843 assert_equal %w(due_date), issue.required_attribute_names(user)
834 end
844 end
835
845
836 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
846 def test_read_only_attribute_names_for_multiple_roles_should_intersect_rules
837 WorkflowPermission.delete_all
847 WorkflowPermission.delete_all
838 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
848 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
839 :role_id => 1, :field_name => 'due_date',
849 :role_id => 1, :field_name => 'due_date',
840 :rule => 'readonly')
850 :rule => 'readonly')
841 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
851 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
842 :role_id => 1, :field_name => 'start_date',
852 :role_id => 1, :field_name => 'start_date',
843 :rule => 'readonly')
853 :rule => 'readonly')
844 user = User.find(2)
854 user = User.find(2)
845 member = Member.find(1)
855 member = Member.find(1)
846 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
856 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1)
847
857
848 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
858 assert_equal %w(due_date start_date), issue.read_only_attribute_names(user).sort
849
859
850 member.role_ids = [1, 2]
860 member.role_ids = [1, 2]
851 member.save!
861 member.save!
852 assert_equal [], issue.read_only_attribute_names(user.reload)
862 assert_equal [], issue.read_only_attribute_names(user.reload)
853
863
854 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
864 WorkflowPermission.create!(:old_status_id => 1, :tracker_id => 1,
855 :role_id => 2, :field_name => 'due_date',
865 :role_id => 2, :field_name => 'due_date',
856 :rule => 'readonly')
866 :rule => 'readonly')
857 assert_equal %w(due_date), issue.read_only_attribute_names(user)
867 assert_equal %w(due_date), issue.read_only_attribute_names(user)
858 end
868 end
859
869
860 def test_copy
870 def test_copy
861 issue = Issue.new.copy_from(1)
871 issue = Issue.new.copy_from(1)
862 assert issue.copy?
872 assert issue.copy?
863 assert issue.save
873 assert issue.save
864 issue.reload
874 issue.reload
865 orig = Issue.find(1)
875 orig = Issue.find(1)
866 assert_equal orig.subject, issue.subject
876 assert_equal orig.subject, issue.subject
867 assert_equal orig.tracker, issue.tracker
877 assert_equal orig.tracker, issue.tracker
868 assert_equal "125", issue.custom_value_for(2).value
878 assert_equal "125", issue.custom_value_for(2).value
869 end
879 end
870
880
871 def test_copy_should_copy_status
881 def test_copy_should_copy_status
872 orig = Issue.find(8)
882 orig = Issue.find(8)
873 assert orig.status != IssueStatus.default
883 assert orig.status != IssueStatus.default
874
884
875 issue = Issue.new.copy_from(orig)
885 issue = Issue.new.copy_from(orig)
876 assert issue.save
886 assert issue.save
877 issue.reload
887 issue.reload
878 assert_equal orig.status, issue.status
888 assert_equal orig.status, issue.status
879 end
889 end
880
890
881 def test_copy_should_add_relation_with_copied_issue
891 def test_copy_should_add_relation_with_copied_issue
882 copied = Issue.find(1)
892 copied = Issue.find(1)
883 issue = Issue.new.copy_from(copied)
893 issue = Issue.new.copy_from(copied)
884 assert issue.save
894 assert issue.save
885 issue.reload
895 issue.reload
886
896
887 assert_equal 1, issue.relations.size
897 assert_equal 1, issue.relations.size
888 relation = issue.relations.first
898 relation = issue.relations.first
889 assert_equal 'copied_to', relation.relation_type
899 assert_equal 'copied_to', relation.relation_type
890 assert_equal copied, relation.issue_from
900 assert_equal copied, relation.issue_from
891 assert_equal issue, relation.issue_to
901 assert_equal issue, relation.issue_to
892 end
902 end
893
903
894 def test_copy_should_copy_subtasks
904 def test_copy_should_copy_subtasks
895 issue = Issue.generate_with_descendants!
905 issue = Issue.generate_with_descendants!
896
906
897 copy = issue.reload.copy
907 copy = issue.reload.copy
898 copy.author = User.find(7)
908 copy.author = User.find(7)
899 assert_difference 'Issue.count', 1+issue.descendants.count do
909 assert_difference 'Issue.count', 1+issue.descendants.count do
900 assert copy.save
910 assert copy.save
901 end
911 end
902 copy.reload
912 copy.reload
903 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
913 assert_equal %w(Child1 Child2), copy.children.map(&:subject).sort
904 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
914 child_copy = copy.children.detect {|c| c.subject == 'Child1'}
905 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
915 assert_equal %w(Child11), child_copy.children.map(&:subject).sort
906 assert_equal copy.author, child_copy.author
916 assert_equal copy.author, child_copy.author
907 end
917 end
908
918
909 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
919 def test_copy_as_a_child_of_copied_issue_should_not_copy_itself
910 parent = Issue.generate!
920 parent = Issue.generate!
911 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
921 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
912 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
922 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
913
923
914 copy = parent.reload.copy
924 copy = parent.reload.copy
915 copy.parent_issue_id = parent.id
925 copy.parent_issue_id = parent.id
916 copy.author = User.find(7)
926 copy.author = User.find(7)
917 assert_difference 'Issue.count', 3 do
927 assert_difference 'Issue.count', 3 do
918 assert copy.save
928 assert copy.save
919 end
929 end
920 parent.reload
930 parent.reload
921 copy.reload
931 copy.reload
922 assert_equal parent, copy.parent
932 assert_equal parent, copy.parent
923 assert_equal 3, parent.children.count
933 assert_equal 3, parent.children.count
924 assert_equal 5, parent.descendants.count
934 assert_equal 5, parent.descendants.count
925 assert_equal 2, copy.children.count
935 assert_equal 2, copy.children.count
926 assert_equal 2, copy.descendants.count
936 assert_equal 2, copy.descendants.count
927 end
937 end
928
938
929 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
939 def test_copy_as_a_descendant_of_copied_issue_should_not_copy_itself
930 parent = Issue.generate!
940 parent = Issue.generate!
931 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
941 child1 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 1')
932 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
942 child2 = Issue.generate!(:parent_issue_id => parent.id, :subject => 'Child 2')
933
943
934 copy = parent.reload.copy
944 copy = parent.reload.copy
935 copy.parent_issue_id = child1.id
945 copy.parent_issue_id = child1.id
936 copy.author = User.find(7)
946 copy.author = User.find(7)
937 assert_difference 'Issue.count', 3 do
947 assert_difference 'Issue.count', 3 do
938 assert copy.save
948 assert copy.save
939 end
949 end
940 parent.reload
950 parent.reload
941 child1.reload
951 child1.reload
942 copy.reload
952 copy.reload
943 assert_equal child1, copy.parent
953 assert_equal child1, copy.parent
944 assert_equal 2, parent.children.count
954 assert_equal 2, parent.children.count
945 assert_equal 5, parent.descendants.count
955 assert_equal 5, parent.descendants.count
946 assert_equal 1, child1.children.count
956 assert_equal 1, child1.children.count
947 assert_equal 3, child1.descendants.count
957 assert_equal 3, child1.descendants.count
948 assert_equal 2, copy.children.count
958 assert_equal 2, copy.children.count
949 assert_equal 2, copy.descendants.count
959 assert_equal 2, copy.descendants.count
950 end
960 end
951
961
952 def test_copy_should_copy_subtasks_to_target_project
962 def test_copy_should_copy_subtasks_to_target_project
953 issue = Issue.generate_with_descendants!
963 issue = Issue.generate_with_descendants!
954
964
955 copy = issue.copy(:project_id => 3)
965 copy = issue.copy(:project_id => 3)
956 assert_difference 'Issue.count', 1+issue.descendants.count do
966 assert_difference 'Issue.count', 1+issue.descendants.count do
957 assert copy.save
967 assert copy.save
958 end
968 end
959 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
969 assert_equal [3], copy.reload.descendants.map(&:project_id).uniq
960 end
970 end
961
971
962 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
972 def test_copy_should_not_copy_subtasks_twice_when_saving_twice
963 issue = Issue.generate_with_descendants!
973 issue = Issue.generate_with_descendants!
964
974
965 copy = issue.reload.copy
975 copy = issue.reload.copy
966 assert_difference 'Issue.count', 1+issue.descendants.count do
976 assert_difference 'Issue.count', 1+issue.descendants.count do
967 assert copy.save
977 assert copy.save
968 assert copy.save
978 assert copy.save
969 end
979 end
970 end
980 end
971
981
972 def test_should_not_call_after_project_change_on_creation
982 def test_should_not_call_after_project_change_on_creation
973 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
983 issue = Issue.new(:project_id => 1, :tracker_id => 1, :status_id => 1,
974 :subject => 'Test', :author_id => 1)
984 :subject => 'Test', :author_id => 1)
975 issue.expects(:after_project_change).never
985 issue.expects(:after_project_change).never
976 issue.save!
986 issue.save!
977 end
987 end
978
988
979 def test_should_not_call_after_project_change_on_update
989 def test_should_not_call_after_project_change_on_update
980 issue = Issue.find(1)
990 issue = Issue.find(1)
981 issue.project = Project.find(1)
991 issue.project = Project.find(1)
982 issue.subject = 'No project change'
992 issue.subject = 'No project change'
983 issue.expects(:after_project_change).never
993 issue.expects(:after_project_change).never
984 issue.save!
994 issue.save!
985 end
995 end
986
996
987 def test_should_call_after_project_change_on_project_change
997 def test_should_call_after_project_change_on_project_change
988 issue = Issue.find(1)
998 issue = Issue.find(1)
989 issue.project = Project.find(2)
999 issue.project = Project.find(2)
990 issue.expects(:after_project_change).once
1000 issue.expects(:after_project_change).once
991 issue.save!
1001 issue.save!
992 end
1002 end
993
1003
994 def test_adding_journal_should_update_timestamp
1004 def test_adding_journal_should_update_timestamp
995 issue = Issue.find(1)
1005 issue = Issue.find(1)
996 updated_on_was = issue.updated_on
1006 updated_on_was = issue.updated_on
997
1007
998 issue.init_journal(User.first, "Adding notes")
1008 issue.init_journal(User.first, "Adding notes")
999 assert_difference 'Journal.count' do
1009 assert_difference 'Journal.count' do
1000 assert issue.save
1010 assert issue.save
1001 end
1011 end
1002 issue.reload
1012 issue.reload
1003
1013
1004 assert_not_equal updated_on_was, issue.updated_on
1014 assert_not_equal updated_on_was, issue.updated_on
1005 end
1015 end
1006
1016
1007 def test_should_close_duplicates
1017 def test_should_close_duplicates
1008 # Create 3 issues
1018 # Create 3 issues
1009 issue1 = Issue.generate!
1019 issue1 = Issue.generate!
1010 issue2 = Issue.generate!
1020 issue2 = Issue.generate!
1011 issue3 = Issue.generate!
1021 issue3 = Issue.generate!
1012
1022
1013 # 2 is a dupe of 1
1023 # 2 is a dupe of 1
1014 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1024 IssueRelation.create!(:issue_from => issue2, :issue_to => issue1,
1015 :relation_type => IssueRelation::TYPE_DUPLICATES)
1025 :relation_type => IssueRelation::TYPE_DUPLICATES)
1016 # And 3 is a dupe of 2
1026 # And 3 is a dupe of 2
1017 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1027 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1018 :relation_type => IssueRelation::TYPE_DUPLICATES)
1028 :relation_type => IssueRelation::TYPE_DUPLICATES)
1019 # And 3 is a dupe of 1 (circular duplicates)
1029 # And 3 is a dupe of 1 (circular duplicates)
1020 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1030 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1,
1021 :relation_type => IssueRelation::TYPE_DUPLICATES)
1031 :relation_type => IssueRelation::TYPE_DUPLICATES)
1022
1032
1023 assert issue1.reload.duplicates.include?(issue2)
1033 assert issue1.reload.duplicates.include?(issue2)
1024
1034
1025 # Closing issue 1
1035 # Closing issue 1
1026 issue1.init_journal(User.first, "Closing issue1")
1036 issue1.init_journal(User.first, "Closing issue1")
1027 issue1.status = IssueStatus.where(:is_closed => true).first
1037 issue1.status = IssueStatus.where(:is_closed => true).first
1028 assert issue1.save
1038 assert issue1.save
1029 # 2 and 3 should be also closed
1039 # 2 and 3 should be also closed
1030 assert issue2.reload.closed?
1040 assert issue2.reload.closed?
1031 assert issue3.reload.closed?
1041 assert issue3.reload.closed?
1032 end
1042 end
1033
1043
1034 def test_should_not_close_duplicated_issue
1044 def test_should_not_close_duplicated_issue
1035 issue1 = Issue.generate!
1045 issue1 = Issue.generate!
1036 issue2 = Issue.generate!
1046 issue2 = Issue.generate!
1037
1047
1038 # 2 is a dupe of 1
1048 # 2 is a dupe of 1
1039 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1049 IssueRelation.create(:issue_from => issue2, :issue_to => issue1,
1040 :relation_type => IssueRelation::TYPE_DUPLICATES)
1050 :relation_type => IssueRelation::TYPE_DUPLICATES)
1041 # 2 is a dup of 1 but 1 is not a duplicate of 2
1051 # 2 is a dup of 1 but 1 is not a duplicate of 2
1042 assert !issue2.reload.duplicates.include?(issue1)
1052 assert !issue2.reload.duplicates.include?(issue1)
1043
1053
1044 # Closing issue 2
1054 # Closing issue 2
1045 issue2.init_journal(User.first, "Closing issue2")
1055 issue2.init_journal(User.first, "Closing issue2")
1046 issue2.status = IssueStatus.where(:is_closed => true).first
1056 issue2.status = IssueStatus.where(:is_closed => true).first
1047 assert issue2.save
1057 assert issue2.save
1048 # 1 should not be also closed
1058 # 1 should not be also closed
1049 assert !issue1.reload.closed?
1059 assert !issue1.reload.closed?
1050 end
1060 end
1051
1061
1052 def test_assignable_versions
1062 def test_assignable_versions
1053 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1063 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1054 :status_id => 1, :fixed_version_id => 1,
1064 :status_id => 1, :fixed_version_id => 1,
1055 :subject => 'New issue')
1065 :subject => 'New issue')
1056 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1066 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
1057 end
1067 end
1058
1068
1059 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1069 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
1060 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1070 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1061 :status_id => 1, :fixed_version_id => 1,
1071 :status_id => 1, :fixed_version_id => 1,
1062 :subject => 'New issue')
1072 :subject => 'New issue')
1063 assert !issue.save
1073 assert !issue.save
1064 assert_not_equal [], issue.errors[:fixed_version_id]
1074 assert_not_equal [], issue.errors[:fixed_version_id]
1065 end
1075 end
1066
1076
1067 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1077 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
1068 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1078 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1069 :status_id => 1, :fixed_version_id => 2,
1079 :status_id => 1, :fixed_version_id => 2,
1070 :subject => 'New issue')
1080 :subject => 'New issue')
1071 assert !issue.save
1081 assert !issue.save
1072 assert_not_equal [], issue.errors[:fixed_version_id]
1082 assert_not_equal [], issue.errors[:fixed_version_id]
1073 end
1083 end
1074
1084
1075 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1085 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
1076 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1086 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
1077 :status_id => 1, :fixed_version_id => 3,
1087 :status_id => 1, :fixed_version_id => 3,
1078 :subject => 'New issue')
1088 :subject => 'New issue')
1079 assert issue.save
1089 assert issue.save
1080 end
1090 end
1081
1091
1082 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1092 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
1083 issue = Issue.find(11)
1093 issue = Issue.find(11)
1084 assert_equal 'closed', issue.fixed_version.status
1094 assert_equal 'closed', issue.fixed_version.status
1085 issue.subject = 'Subject changed'
1095 issue.subject = 'Subject changed'
1086 assert issue.save
1096 assert issue.save
1087 end
1097 end
1088
1098
1089 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1099 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
1090 issue = Issue.find(11)
1100 issue = Issue.find(11)
1091 issue.status_id = 1
1101 issue.status_id = 1
1092 assert !issue.save
1102 assert !issue.save
1093 assert_not_equal [], issue.errors[:base]
1103 assert_not_equal [], issue.errors[:base]
1094 end
1104 end
1095
1105
1096 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1106 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
1097 issue = Issue.find(11)
1107 issue = Issue.find(11)
1098 issue.status_id = 1
1108 issue.status_id = 1
1099 issue.fixed_version_id = 3
1109 issue.fixed_version_id = 3
1100 assert issue.save
1110 assert issue.save
1101 end
1111 end
1102
1112
1103 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1113 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
1104 issue = Issue.find(12)
1114 issue = Issue.find(12)
1105 assert_equal 'locked', issue.fixed_version.status
1115 assert_equal 'locked', issue.fixed_version.status
1106 issue.status_id = 1
1116 issue.status_id = 1
1107 assert issue.save
1117 assert issue.save
1108 end
1118 end
1109
1119
1110 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1120 def test_should_not_be_able_to_keep_unshared_version_when_changing_project
1111 issue = Issue.find(2)
1121 issue = Issue.find(2)
1112 assert_equal 2, issue.fixed_version_id
1122 assert_equal 2, issue.fixed_version_id
1113 issue.project_id = 3
1123 issue.project_id = 3
1114 assert_nil issue.fixed_version_id
1124 assert_nil issue.fixed_version_id
1115 issue.fixed_version_id = 2
1125 issue.fixed_version_id = 2
1116 assert !issue.save
1126 assert !issue.save
1117 assert_include 'Target version is not included in the list', issue.errors.full_messages
1127 assert_include 'Target version is not included in the list', issue.errors.full_messages
1118 end
1128 end
1119
1129
1120 def test_should_keep_shared_version_when_changing_project
1130 def test_should_keep_shared_version_when_changing_project
1121 Version.find(2).update_attribute :sharing, 'tree'
1131 Version.find(2).update_attribute :sharing, 'tree'
1122
1132
1123 issue = Issue.find(2)
1133 issue = Issue.find(2)
1124 assert_equal 2, issue.fixed_version_id
1134 assert_equal 2, issue.fixed_version_id
1125 issue.project_id = 3
1135 issue.project_id = 3
1126 assert_equal 2, issue.fixed_version_id
1136 assert_equal 2, issue.fixed_version_id
1127 assert issue.save
1137 assert issue.save
1128 end
1138 end
1129
1139
1130 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
1140 def test_allowed_target_projects_on_move_should_include_projects_with_issue_tracking_enabled
1131 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1141 assert_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1132 end
1142 end
1133
1143
1134 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
1144 def test_allowed_target_projects_on_move_should_not_include_projects_with_issue_tracking_disabled
1135 Project.find(2).disable_module! :issue_tracking
1145 Project.find(2).disable_module! :issue_tracking
1136 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1146 assert_not_include Project.find(2), Issue.allowed_target_projects_on_move(User.find(2))
1137 end
1147 end
1138
1148
1139 def test_move_to_another_project_with_same_category
1149 def test_move_to_another_project_with_same_category
1140 issue = Issue.find(1)
1150 issue = Issue.find(1)
1141 issue.project = Project.find(2)
1151 issue.project = Project.find(2)
1142 assert issue.save
1152 assert issue.save
1143 issue.reload
1153 issue.reload
1144 assert_equal 2, issue.project_id
1154 assert_equal 2, issue.project_id
1145 # Category changes
1155 # Category changes
1146 assert_equal 4, issue.category_id
1156 assert_equal 4, issue.category_id
1147 # Make sure time entries were move to the target project
1157 # Make sure time entries were move to the target project
1148 assert_equal 2, issue.time_entries.first.project_id
1158 assert_equal 2, issue.time_entries.first.project_id
1149 end
1159 end
1150
1160
1151 def test_move_to_another_project_without_same_category
1161 def test_move_to_another_project_without_same_category
1152 issue = Issue.find(2)
1162 issue = Issue.find(2)
1153 issue.project = Project.find(2)
1163 issue.project = Project.find(2)
1154 assert issue.save
1164 assert issue.save
1155 issue.reload
1165 issue.reload
1156 assert_equal 2, issue.project_id
1166 assert_equal 2, issue.project_id
1157 # Category cleared
1167 # Category cleared
1158 assert_nil issue.category_id
1168 assert_nil issue.category_id
1159 end
1169 end
1160
1170
1161 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1171 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
1162 issue = Issue.find(1)
1172 issue = Issue.find(1)
1163 issue.update_attribute(:fixed_version_id, 1)
1173 issue.update_attribute(:fixed_version_id, 1)
1164 issue.project = Project.find(2)
1174 issue.project = Project.find(2)
1165 assert issue.save
1175 assert issue.save
1166 issue.reload
1176 issue.reload
1167 assert_equal 2, issue.project_id
1177 assert_equal 2, issue.project_id
1168 # Cleared fixed_version
1178 # Cleared fixed_version
1169 assert_equal nil, issue.fixed_version
1179 assert_equal nil, issue.fixed_version
1170 end
1180 end
1171
1181
1172 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1182 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
1173 issue = Issue.find(1)
1183 issue = Issue.find(1)
1174 issue.update_attribute(:fixed_version_id, 4)
1184 issue.update_attribute(:fixed_version_id, 4)
1175 issue.project = Project.find(5)
1185 issue.project = Project.find(5)
1176 assert issue.save
1186 assert issue.save
1177 issue.reload
1187 issue.reload
1178 assert_equal 5, issue.project_id
1188 assert_equal 5, issue.project_id
1179 # Keep fixed_version
1189 # Keep fixed_version
1180 assert_equal 4, issue.fixed_version_id
1190 assert_equal 4, issue.fixed_version_id
1181 end
1191 end
1182
1192
1183 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1193 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
1184 issue = Issue.find(1)
1194 issue = Issue.find(1)
1185 issue.update_attribute(:fixed_version_id, 1)
1195 issue.update_attribute(:fixed_version_id, 1)
1186 issue.project = Project.find(5)
1196 issue.project = Project.find(5)
1187 assert issue.save
1197 assert issue.save
1188 issue.reload
1198 issue.reload
1189 assert_equal 5, issue.project_id
1199 assert_equal 5, issue.project_id
1190 # Cleared fixed_version
1200 # Cleared fixed_version
1191 assert_equal nil, issue.fixed_version
1201 assert_equal nil, issue.fixed_version
1192 end
1202 end
1193
1203
1194 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1204 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
1195 issue = Issue.find(1)
1205 issue = Issue.find(1)
1196 issue.update_attribute(:fixed_version_id, 7)
1206 issue.update_attribute(:fixed_version_id, 7)
1197 issue.project = Project.find(2)
1207 issue.project = Project.find(2)
1198 assert issue.save
1208 assert issue.save
1199 issue.reload
1209 issue.reload
1200 assert_equal 2, issue.project_id
1210 assert_equal 2, issue.project_id
1201 # Keep fixed_version
1211 # Keep fixed_version
1202 assert_equal 7, issue.fixed_version_id
1212 assert_equal 7, issue.fixed_version_id
1203 end
1213 end
1204
1214
1205 def test_move_to_another_project_should_keep_parent_if_valid
1215 def test_move_to_another_project_should_keep_parent_if_valid
1206 issue = Issue.find(1)
1216 issue = Issue.find(1)
1207 issue.update_attribute(:parent_issue_id, 2)
1217 issue.update_attribute(:parent_issue_id, 2)
1208 issue.project = Project.find(3)
1218 issue.project = Project.find(3)
1209 assert issue.save
1219 assert issue.save
1210 issue.reload
1220 issue.reload
1211 assert_equal 2, issue.parent_id
1221 assert_equal 2, issue.parent_id
1212 end
1222 end
1213
1223
1214 def test_move_to_another_project_should_clear_parent_if_not_valid
1224 def test_move_to_another_project_should_clear_parent_if_not_valid
1215 issue = Issue.find(1)
1225 issue = Issue.find(1)
1216 issue.update_attribute(:parent_issue_id, 2)
1226 issue.update_attribute(:parent_issue_id, 2)
1217 issue.project = Project.find(2)
1227 issue.project = Project.find(2)
1218 assert issue.save
1228 assert issue.save
1219 issue.reload
1229 issue.reload
1220 assert_nil issue.parent_id
1230 assert_nil issue.parent_id
1221 end
1231 end
1222
1232
1223 def test_move_to_another_project_with_disabled_tracker
1233 def test_move_to_another_project_with_disabled_tracker
1224 issue = Issue.find(1)
1234 issue = Issue.find(1)
1225 target = Project.find(2)
1235 target = Project.find(2)
1226 target.tracker_ids = [3]
1236 target.tracker_ids = [3]
1227 target.save
1237 target.save
1228 issue.project = target
1238 issue.project = target
1229 assert issue.save
1239 assert issue.save
1230 issue.reload
1240 issue.reload
1231 assert_equal 2, issue.project_id
1241 assert_equal 2, issue.project_id
1232 assert_equal 3, issue.tracker_id
1242 assert_equal 3, issue.tracker_id
1233 end
1243 end
1234
1244
1235 def test_copy_to_the_same_project
1245 def test_copy_to_the_same_project
1236 issue = Issue.find(1)
1246 issue = Issue.find(1)
1237 copy = issue.copy
1247 copy = issue.copy
1238 assert_difference 'Issue.count' do
1248 assert_difference 'Issue.count' do
1239 copy.save!
1249 copy.save!
1240 end
1250 end
1241 assert_kind_of Issue, copy
1251 assert_kind_of Issue, copy
1242 assert_equal issue.project, copy.project
1252 assert_equal issue.project, copy.project
1243 assert_equal "125", copy.custom_value_for(2).value
1253 assert_equal "125", copy.custom_value_for(2).value
1244 end
1254 end
1245
1255
1246 def test_copy_to_another_project_and_tracker
1256 def test_copy_to_another_project_and_tracker
1247 issue = Issue.find(1)
1257 issue = Issue.find(1)
1248 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1258 copy = issue.copy(:project_id => 3, :tracker_id => 2)
1249 assert_difference 'Issue.count' do
1259 assert_difference 'Issue.count' do
1250 copy.save!
1260 copy.save!
1251 end
1261 end
1252 copy.reload
1262 copy.reload
1253 assert_kind_of Issue, copy
1263 assert_kind_of Issue, copy
1254 assert_equal Project.find(3), copy.project
1264 assert_equal Project.find(3), copy.project
1255 assert_equal Tracker.find(2), copy.tracker
1265 assert_equal Tracker.find(2), copy.tracker
1256 # Custom field #2 is not associated with target tracker
1266 # Custom field #2 is not associated with target tracker
1257 assert_nil copy.custom_value_for(2)
1267 assert_nil copy.custom_value_for(2)
1258 end
1268 end
1259
1269
1260 test "#copy should not create a journal" do
1270 test "#copy should not create a journal" do
1261 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1271 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1262 copy.save!
1272 copy.save!
1263 assert_equal 0, copy.reload.journals.size
1273 assert_equal 0, copy.reload.journals.size
1264 end
1274 end
1265
1275
1266 test "#copy should allow assigned_to changes" do
1276 test "#copy should allow assigned_to changes" do
1267 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1277 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :assigned_to_id => 3)
1268 assert_equal 3, copy.assigned_to_id
1278 assert_equal 3, copy.assigned_to_id
1269 end
1279 end
1270
1280
1271 test "#copy should allow status changes" do
1281 test "#copy should allow status changes" do
1272 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1282 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :status_id => 2)
1273 assert_equal 2, copy.status_id
1283 assert_equal 2, copy.status_id
1274 end
1284 end
1275
1285
1276 test "#copy should allow start date changes" do
1286 test "#copy should allow start date changes" do
1277 date = Date.today
1287 date = Date.today
1278 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1288 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1279 assert_equal date, copy.start_date
1289 assert_equal date, copy.start_date
1280 end
1290 end
1281
1291
1282 test "#copy should allow due date changes" do
1292 test "#copy should allow due date changes" do
1283 date = Date.today
1293 date = Date.today
1284 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1294 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :due_date => date)
1285 assert_equal date, copy.due_date
1295 assert_equal date, copy.due_date
1286 end
1296 end
1287
1297
1288 test "#copy should set current user as author" do
1298 test "#copy should set current user as author" do
1289 User.current = User.find(9)
1299 User.current = User.find(9)
1290 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1300 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2)
1291 assert_equal User.current, copy.author
1301 assert_equal User.current, copy.author
1292 end
1302 end
1293
1303
1294 test "#copy should create a journal with notes" do
1304 test "#copy should create a journal with notes" do
1295 date = Date.today
1305 date = Date.today
1296 notes = "Notes added when copying"
1306 notes = "Notes added when copying"
1297 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1307 copy = Issue.find(1).copy(:project_id => 3, :tracker_id => 2, :start_date => date)
1298 copy.init_journal(User.current, notes)
1308 copy.init_journal(User.current, notes)
1299 copy.save!
1309 copy.save!
1300
1310
1301 assert_equal 1, copy.journals.size
1311 assert_equal 1, copy.journals.size
1302 journal = copy.journals.first
1312 journal = copy.journals.first
1303 assert_equal 0, journal.details.size
1313 assert_equal 0, journal.details.size
1304 assert_equal notes, journal.notes
1314 assert_equal notes, journal.notes
1305 end
1315 end
1306
1316
1307 def test_valid_parent_project
1317 def test_valid_parent_project
1308 issue = Issue.find(1)
1318 issue = Issue.find(1)
1309 issue_in_same_project = Issue.find(2)
1319 issue_in_same_project = Issue.find(2)
1310 issue_in_child_project = Issue.find(5)
1320 issue_in_child_project = Issue.find(5)
1311 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1321 issue_in_grandchild_project = Issue.generate!(:project_id => 6, :tracker_id => 1)
1312 issue_in_other_child_project = Issue.find(6)
1322 issue_in_other_child_project = Issue.find(6)
1313 issue_in_different_tree = Issue.find(4)
1323 issue_in_different_tree = Issue.find(4)
1314
1324
1315 with_settings :cross_project_subtasks => '' do
1325 with_settings :cross_project_subtasks => '' do
1316 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1326 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1317 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1327 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1318 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1328 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1319 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1329 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1320 end
1330 end
1321
1331
1322 with_settings :cross_project_subtasks => 'system' do
1332 with_settings :cross_project_subtasks => 'system' do
1323 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1333 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1324 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1334 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1325 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1335 assert_equal true, issue.valid_parent_project?(issue_in_different_tree)
1326 end
1336 end
1327
1337
1328 with_settings :cross_project_subtasks => 'tree' do
1338 with_settings :cross_project_subtasks => 'tree' do
1329 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1339 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1330 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1340 assert_equal true, issue.valid_parent_project?(issue_in_child_project)
1331 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1341 assert_equal true, issue.valid_parent_project?(issue_in_grandchild_project)
1332 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1342 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1333
1343
1334 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1344 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_same_project)
1335 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1345 assert_equal true, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1336 end
1346 end
1337
1347
1338 with_settings :cross_project_subtasks => 'descendants' do
1348 with_settings :cross_project_subtasks => 'descendants' do
1339 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1349 assert_equal true, issue.valid_parent_project?(issue_in_same_project)
1340 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1350 assert_equal false, issue.valid_parent_project?(issue_in_child_project)
1341 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1351 assert_equal false, issue.valid_parent_project?(issue_in_grandchild_project)
1342 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1352 assert_equal false, issue.valid_parent_project?(issue_in_different_tree)
1343
1353
1344 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1354 assert_equal true, issue_in_child_project.valid_parent_project?(issue)
1345 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1355 assert_equal false, issue_in_child_project.valid_parent_project?(issue_in_other_child_project)
1346 end
1356 end
1347 end
1357 end
1348
1358
1349 def test_recipients_should_include_previous_assignee
1359 def test_recipients_should_include_previous_assignee
1350 user = User.find(3)
1360 user = User.find(3)
1351 user.members.update_all ["mail_notification = ?", false]
1361 user.members.update_all ["mail_notification = ?", false]
1352 user.update_attribute :mail_notification, 'only_assigned'
1362 user.update_attribute :mail_notification, 'only_assigned'
1353
1363
1354 issue = Issue.find(2)
1364 issue = Issue.find(2)
1355 issue.assigned_to = nil
1365 issue.assigned_to = nil
1356 assert_include user.mail, issue.recipients
1366 assert_include user.mail, issue.recipients
1357 issue.save!
1367 issue.save!
1358 assert !issue.recipients.include?(user.mail)
1368 assert !issue.recipients.include?(user.mail)
1359 end
1369 end
1360
1370
1361 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1371 def test_recipients_should_not_include_users_that_cannot_view_the_issue
1362 issue = Issue.find(12)
1372 issue = Issue.find(12)
1363 assert issue.recipients.include?(issue.author.mail)
1373 assert issue.recipients.include?(issue.author.mail)
1364 # copy the issue to a private project
1374 # copy the issue to a private project
1365 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1375 copy = issue.copy(:project_id => 5, :tracker_id => 2)
1366 # author is not a member of project anymore
1376 # author is not a member of project anymore
1367 assert !copy.recipients.include?(copy.author.mail)
1377 assert !copy.recipients.include?(copy.author.mail)
1368 end
1378 end
1369
1379
1370 def test_recipients_should_include_the_assigned_group_members
1380 def test_recipients_should_include_the_assigned_group_members
1371 group_member = User.generate!
1381 group_member = User.generate!
1372 group = Group.generate!
1382 group = Group.generate!
1373 group.users << group_member
1383 group.users << group_member
1374
1384
1375 issue = Issue.find(12)
1385 issue = Issue.find(12)
1376 issue.assigned_to = group
1386 issue.assigned_to = group
1377 assert issue.recipients.include?(group_member.mail)
1387 assert issue.recipients.include?(group_member.mail)
1378 end
1388 end
1379
1389
1380 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1390 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
1381 user = User.find(3)
1391 user = User.find(3)
1382 issue = Issue.find(9)
1392 issue = Issue.find(9)
1383 Watcher.create!(:user => user, :watchable => issue)
1393 Watcher.create!(:user => user, :watchable => issue)
1384 assert issue.watched_by?(user)
1394 assert issue.watched_by?(user)
1385 assert !issue.watcher_recipients.include?(user.mail)
1395 assert !issue.watcher_recipients.include?(user.mail)
1386 end
1396 end
1387
1397
1388 def test_issue_destroy
1398 def test_issue_destroy
1389 Issue.find(1).destroy
1399 Issue.find(1).destroy
1390 assert_nil Issue.find_by_id(1)
1400 assert_nil Issue.find_by_id(1)
1391 assert_nil TimeEntry.find_by_issue_id(1)
1401 assert_nil TimeEntry.find_by_issue_id(1)
1392 end
1402 end
1393
1403
1394 def test_destroy_should_delete_time_entries_custom_values
1404 def test_destroy_should_delete_time_entries_custom_values
1395 issue = Issue.generate!
1405 issue = Issue.generate!
1396 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1406 time_entry = TimeEntry.generate!(:issue => issue, :custom_field_values => {10 => '1'})
1397
1407
1398 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1408 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
1399 assert issue.destroy
1409 assert issue.destroy
1400 end
1410 end
1401 end
1411 end
1402
1412
1403 def test_destroying_a_deleted_issue_should_not_raise_an_error
1413 def test_destroying_a_deleted_issue_should_not_raise_an_error
1404 issue = Issue.find(1)
1414 issue = Issue.find(1)
1405 Issue.find(1).destroy
1415 Issue.find(1).destroy
1406
1416
1407 assert_nothing_raised do
1417 assert_nothing_raised do
1408 assert_no_difference 'Issue.count' do
1418 assert_no_difference 'Issue.count' do
1409 issue.destroy
1419 issue.destroy
1410 end
1420 end
1411 assert issue.destroyed?
1421 assert issue.destroyed?
1412 end
1422 end
1413 end
1423 end
1414
1424
1415 def test_destroying_a_stale_issue_should_not_raise_an_error
1425 def test_destroying_a_stale_issue_should_not_raise_an_error
1416 issue = Issue.find(1)
1426 issue = Issue.find(1)
1417 Issue.find(1).update_attribute :subject, "Updated"
1427 Issue.find(1).update_attribute :subject, "Updated"
1418
1428
1419 assert_nothing_raised do
1429 assert_nothing_raised do
1420 assert_difference 'Issue.count', -1 do
1430 assert_difference 'Issue.count', -1 do
1421 issue.destroy
1431 issue.destroy
1422 end
1432 end
1423 assert issue.destroyed?
1433 assert issue.destroyed?
1424 end
1434 end
1425 end
1435 end
1426
1436
1427 def test_blocked
1437 def test_blocked
1428 blocked_issue = Issue.find(9)
1438 blocked_issue = Issue.find(9)
1429 blocking_issue = Issue.find(10)
1439 blocking_issue = Issue.find(10)
1430
1440
1431 assert blocked_issue.blocked?
1441 assert blocked_issue.blocked?
1432 assert !blocking_issue.blocked?
1442 assert !blocking_issue.blocked?
1433 end
1443 end
1434
1444
1435 def test_blocked_issues_dont_allow_closed_statuses
1445 def test_blocked_issues_dont_allow_closed_statuses
1436 blocked_issue = Issue.find(9)
1446 blocked_issue = Issue.find(9)
1437
1447
1438 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1448 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
1439 assert !allowed_statuses.empty?
1449 assert !allowed_statuses.empty?
1440 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1450 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1441 assert closed_statuses.empty?
1451 assert closed_statuses.empty?
1442 end
1452 end
1443
1453
1444 def test_unblocked_issues_allow_closed_statuses
1454 def test_unblocked_issues_allow_closed_statuses
1445 blocking_issue = Issue.find(10)
1455 blocking_issue = Issue.find(10)
1446
1456
1447 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1457 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
1448 assert !allowed_statuses.empty?
1458 assert !allowed_statuses.empty?
1449 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1459 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
1450 assert !closed_statuses.empty?
1460 assert !closed_statuses.empty?
1451 end
1461 end
1452
1462
1453 def test_reschedule_an_issue_without_dates
1463 def test_reschedule_an_issue_without_dates
1454 with_settings :non_working_week_days => [] do
1464 with_settings :non_working_week_days => [] do
1455 issue = Issue.new(:start_date => nil, :due_date => nil)
1465 issue = Issue.new(:start_date => nil, :due_date => nil)
1456 issue.reschedule_on '2012-10-09'.to_date
1466 issue.reschedule_on '2012-10-09'.to_date
1457 assert_equal '2012-10-09'.to_date, issue.start_date
1467 assert_equal '2012-10-09'.to_date, issue.start_date
1458 assert_equal '2012-10-09'.to_date, issue.due_date
1468 assert_equal '2012-10-09'.to_date, issue.due_date
1459 end
1469 end
1460
1470
1461 with_settings :non_working_week_days => %w(6 7) do
1471 with_settings :non_working_week_days => %w(6 7) do
1462 issue = Issue.new(:start_date => nil, :due_date => nil)
1472 issue = Issue.new(:start_date => nil, :due_date => nil)
1463 issue.reschedule_on '2012-10-09'.to_date
1473 issue.reschedule_on '2012-10-09'.to_date
1464 assert_equal '2012-10-09'.to_date, issue.start_date
1474 assert_equal '2012-10-09'.to_date, issue.start_date
1465 assert_equal '2012-10-09'.to_date, issue.due_date
1475 assert_equal '2012-10-09'.to_date, issue.due_date
1466
1476
1467 issue = Issue.new(:start_date => nil, :due_date => nil)
1477 issue = Issue.new(:start_date => nil, :due_date => nil)
1468 issue.reschedule_on '2012-10-13'.to_date
1478 issue.reschedule_on '2012-10-13'.to_date
1469 assert_equal '2012-10-15'.to_date, issue.start_date
1479 assert_equal '2012-10-15'.to_date, issue.start_date
1470 assert_equal '2012-10-15'.to_date, issue.due_date
1480 assert_equal '2012-10-15'.to_date, issue.due_date
1471 end
1481 end
1472 end
1482 end
1473
1483
1474 def test_reschedule_an_issue_with_start_date
1484 def test_reschedule_an_issue_with_start_date
1475 with_settings :non_working_week_days => [] do
1485 with_settings :non_working_week_days => [] do
1476 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1486 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1477 issue.reschedule_on '2012-10-13'.to_date
1487 issue.reschedule_on '2012-10-13'.to_date
1478 assert_equal '2012-10-13'.to_date, issue.start_date
1488 assert_equal '2012-10-13'.to_date, issue.start_date
1479 assert_equal '2012-10-13'.to_date, issue.due_date
1489 assert_equal '2012-10-13'.to_date, issue.due_date
1480 end
1490 end
1481
1491
1482 with_settings :non_working_week_days => %w(6 7) do
1492 with_settings :non_working_week_days => %w(6 7) do
1483 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1493 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1484 issue.reschedule_on '2012-10-11'.to_date
1494 issue.reschedule_on '2012-10-11'.to_date
1485 assert_equal '2012-10-11'.to_date, issue.start_date
1495 assert_equal '2012-10-11'.to_date, issue.start_date
1486 assert_equal '2012-10-11'.to_date, issue.due_date
1496 assert_equal '2012-10-11'.to_date, issue.due_date
1487
1497
1488 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1498 issue = Issue.new(:start_date => '2012-10-09', :due_date => nil)
1489 issue.reschedule_on '2012-10-13'.to_date
1499 issue.reschedule_on '2012-10-13'.to_date
1490 assert_equal '2012-10-15'.to_date, issue.start_date
1500 assert_equal '2012-10-15'.to_date, issue.start_date
1491 assert_equal '2012-10-15'.to_date, issue.due_date
1501 assert_equal '2012-10-15'.to_date, issue.due_date
1492 end
1502 end
1493 end
1503 end
1494
1504
1495 def test_reschedule_an_issue_with_start_and_due_dates
1505 def test_reschedule_an_issue_with_start_and_due_dates
1496 with_settings :non_working_week_days => [] do
1506 with_settings :non_working_week_days => [] do
1497 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1507 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-15')
1498 issue.reschedule_on '2012-10-13'.to_date
1508 issue.reschedule_on '2012-10-13'.to_date
1499 assert_equal '2012-10-13'.to_date, issue.start_date
1509 assert_equal '2012-10-13'.to_date, issue.start_date
1500 assert_equal '2012-10-19'.to_date, issue.due_date
1510 assert_equal '2012-10-19'.to_date, issue.due_date
1501 end
1511 end
1502
1512
1503 with_settings :non_working_week_days => %w(6 7) do
1513 with_settings :non_working_week_days => %w(6 7) do
1504 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1514 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19') # 8 working days
1505 issue.reschedule_on '2012-10-11'.to_date
1515 issue.reschedule_on '2012-10-11'.to_date
1506 assert_equal '2012-10-11'.to_date, issue.start_date
1516 assert_equal '2012-10-11'.to_date, issue.start_date
1507 assert_equal '2012-10-23'.to_date, issue.due_date
1517 assert_equal '2012-10-23'.to_date, issue.due_date
1508
1518
1509 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1519 issue = Issue.new(:start_date => '2012-10-09', :due_date => '2012-10-19')
1510 issue.reschedule_on '2012-10-13'.to_date
1520 issue.reschedule_on '2012-10-13'.to_date
1511 assert_equal '2012-10-15'.to_date, issue.start_date
1521 assert_equal '2012-10-15'.to_date, issue.start_date
1512 assert_equal '2012-10-25'.to_date, issue.due_date
1522 assert_equal '2012-10-25'.to_date, issue.due_date
1513 end
1523 end
1514 end
1524 end
1515
1525
1516 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1526 def test_rescheduling_an_issue_to_a_later_due_date_should_reschedule_following_issue
1517 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1527 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1518 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1528 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1519 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1529 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1520 :relation_type => IssueRelation::TYPE_PRECEDES)
1530 :relation_type => IssueRelation::TYPE_PRECEDES)
1521 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1531 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1522
1532
1523 issue1.reload
1533 issue1.reload
1524 issue1.due_date = '2012-10-23'
1534 issue1.due_date = '2012-10-23'
1525 issue1.save!
1535 issue1.save!
1526 issue2.reload
1536 issue2.reload
1527 assert_equal Date.parse('2012-10-24'), issue2.start_date
1537 assert_equal Date.parse('2012-10-24'), issue2.start_date
1528 assert_equal Date.parse('2012-10-26'), issue2.due_date
1538 assert_equal Date.parse('2012-10-26'), issue2.due_date
1529 end
1539 end
1530
1540
1531 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1541 def test_rescheduling_an_issue_to_an_earlier_due_date_should_reschedule_following_issue
1532 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1542 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1533 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1543 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1534 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1544 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1535 :relation_type => IssueRelation::TYPE_PRECEDES)
1545 :relation_type => IssueRelation::TYPE_PRECEDES)
1536 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1546 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1537
1547
1538 issue1.reload
1548 issue1.reload
1539 issue1.start_date = '2012-09-17'
1549 issue1.start_date = '2012-09-17'
1540 issue1.due_date = '2012-09-18'
1550 issue1.due_date = '2012-09-18'
1541 issue1.save!
1551 issue1.save!
1542 issue2.reload
1552 issue2.reload
1543 assert_equal Date.parse('2012-09-19'), issue2.start_date
1553 assert_equal Date.parse('2012-09-19'), issue2.start_date
1544 assert_equal Date.parse('2012-09-21'), issue2.due_date
1554 assert_equal Date.parse('2012-09-21'), issue2.due_date
1545 end
1555 end
1546
1556
1547 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1557 def test_rescheduling_reschedule_following_issue_earlier_should_consider_other_preceding_issues
1548 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1558 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1549 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1559 issue2 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1550 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1560 issue3 = Issue.generate!(:start_date => '2012-10-01', :due_date => '2012-10-02')
1551 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1561 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1552 :relation_type => IssueRelation::TYPE_PRECEDES)
1562 :relation_type => IssueRelation::TYPE_PRECEDES)
1553 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1563 IssueRelation.create!(:issue_from => issue3, :issue_to => issue2,
1554 :relation_type => IssueRelation::TYPE_PRECEDES)
1564 :relation_type => IssueRelation::TYPE_PRECEDES)
1555 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1565 assert_equal Date.parse('2012-10-18'), issue2.reload.start_date
1556
1566
1557 issue1.reload
1567 issue1.reload
1558 issue1.start_date = '2012-09-17'
1568 issue1.start_date = '2012-09-17'
1559 issue1.due_date = '2012-09-18'
1569 issue1.due_date = '2012-09-18'
1560 issue1.save!
1570 issue1.save!
1561 issue2.reload
1571 issue2.reload
1562 # Issue 2 must start after Issue 3
1572 # Issue 2 must start after Issue 3
1563 assert_equal Date.parse('2012-10-03'), issue2.start_date
1573 assert_equal Date.parse('2012-10-03'), issue2.start_date
1564 assert_equal Date.parse('2012-10-05'), issue2.due_date
1574 assert_equal Date.parse('2012-10-05'), issue2.due_date
1565 end
1575 end
1566
1576
1567 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1577 def test_rescheduling_a_stale_issue_should_not_raise_an_error
1568 with_settings :non_working_week_days => [] do
1578 with_settings :non_working_week_days => [] do
1569 stale = Issue.find(1)
1579 stale = Issue.find(1)
1570 issue = Issue.find(1)
1580 issue = Issue.find(1)
1571 issue.subject = "Updated"
1581 issue.subject = "Updated"
1572 issue.save!
1582 issue.save!
1573 date = 10.days.from_now.to_date
1583 date = 10.days.from_now.to_date
1574 assert_nothing_raised do
1584 assert_nothing_raised do
1575 stale.reschedule_on!(date)
1585 stale.reschedule_on!(date)
1576 end
1586 end
1577 assert_equal date, stale.reload.start_date
1587 assert_equal date, stale.reload.start_date
1578 end
1588 end
1579 end
1589 end
1580
1590
1581 def test_child_issue_should_consider_parent_soonest_start_on_create
1591 def test_child_issue_should_consider_parent_soonest_start_on_create
1582 set_language_if_valid 'en'
1592 set_language_if_valid 'en'
1583 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1593 issue1 = Issue.generate!(:start_date => '2012-10-15', :due_date => '2012-10-17')
1584 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
1594 issue2 = Issue.generate!(:start_date => '2012-10-18', :due_date => '2012-10-20')
1585 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1595 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2,
1586 :relation_type => IssueRelation::TYPE_PRECEDES)
1596 :relation_type => IssueRelation::TYPE_PRECEDES)
1587 issue1.reload
1597 issue1.reload
1588 issue2.reload
1598 issue2.reload
1589 assert_equal Date.parse('2012-10-18'), issue2.start_date
1599 assert_equal Date.parse('2012-10-18'), issue2.start_date
1590
1600
1591 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
1601 child = Issue.new(:parent_issue_id => issue2.id, :start_date => '2012-10-16',
1592 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
1602 :project_id => 1, :tracker_id => 1, :status_id => 1, :subject => 'Child', :author_id => 1)
1593 assert !child.valid?
1603 assert !child.valid?
1594 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
1604 assert_include 'Start date cannot be earlier than 10/18/2012 because of preceding issues', child.errors.full_messages
1595 assert_equal Date.parse('2012-10-18'), child.soonest_start
1605 assert_equal Date.parse('2012-10-18'), child.soonest_start
1596 child.start_date = '2012-10-18'
1606 child.start_date = '2012-10-18'
1597 assert child.save
1607 assert child.save
1598 end
1608 end
1599
1609
1600 def test_setting_parent_to_a_dependent_issue_should_not_validate
1610 def test_setting_parent_to_a_dependent_issue_should_not_validate
1601 set_language_if_valid 'en'
1611 set_language_if_valid 'en'
1602 issue1 = Issue.generate!
1612 issue1 = Issue.generate!
1603 issue2 = Issue.generate!
1613 issue2 = Issue.generate!
1604 issue3 = Issue.generate!
1614 issue3 = Issue.generate!
1605 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1615 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1606 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
1616 IssueRelation.create!(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_PRECEDES)
1607 issue3.reload
1617 issue3.reload
1608 issue3.parent_issue_id = issue2.id
1618 issue3.parent_issue_id = issue2.id
1609 assert !issue3.valid?
1619 assert !issue3.valid?
1610 assert_include 'Parent task is invalid', issue3.errors.full_messages
1620 assert_include 'Parent task is invalid', issue3.errors.full_messages
1611 end
1621 end
1612
1622
1613 def test_setting_parent_should_not_allow_circular_dependency
1623 def test_setting_parent_should_not_allow_circular_dependency
1614 set_language_if_valid 'en'
1624 set_language_if_valid 'en'
1615 issue1 = Issue.generate!
1625 issue1 = Issue.generate!
1616 issue2 = Issue.generate!
1626 issue2 = Issue.generate!
1617 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1627 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
1618 issue3 = Issue.generate!
1628 issue3 = Issue.generate!
1619 issue2.reload
1629 issue2.reload
1620 issue2.parent_issue_id = issue3.id
1630 issue2.parent_issue_id = issue3.id
1621 issue2.save!
1631 issue2.save!
1622 issue4 = Issue.generate!
1632 issue4 = Issue.generate!
1623 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
1633 IssueRelation.create!(:issue_from => issue3, :issue_to => issue4, :relation_type => IssueRelation::TYPE_PRECEDES)
1624 issue4.reload
1634 issue4.reload
1625 issue4.parent_issue_id = issue1.id
1635 issue4.parent_issue_id = issue1.id
1626 assert !issue4.valid?
1636 assert !issue4.valid?
1627 assert_include 'Parent task is invalid', issue4.errors.full_messages
1637 assert_include 'Parent task is invalid', issue4.errors.full_messages
1628 end
1638 end
1629
1639
1630 def test_overdue
1640 def test_overdue
1631 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1641 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
1632 assert !Issue.new(:due_date => Date.today).overdue?
1642 assert !Issue.new(:due_date => Date.today).overdue?
1633 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1643 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
1634 assert !Issue.new(:due_date => nil).overdue?
1644 assert !Issue.new(:due_date => nil).overdue?
1635 assert !Issue.new(:due_date => 1.day.ago.to_date,
1645 assert !Issue.new(:due_date => 1.day.ago.to_date,
1636 :status => IssueStatus.where(:is_closed => true).first
1646 :status => IssueStatus.where(:is_closed => true).first
1637 ).overdue?
1647 ).overdue?
1638 end
1648 end
1639
1649
1640 test "#behind_schedule? should be false if the issue has no start_date" do
1650 test "#behind_schedule? should be false if the issue has no start_date" do
1641 assert !Issue.new(:start_date => nil,
1651 assert !Issue.new(:start_date => nil,
1642 :due_date => 1.day.from_now.to_date,
1652 :due_date => 1.day.from_now.to_date,
1643 :done_ratio => 0).behind_schedule?
1653 :done_ratio => 0).behind_schedule?
1644 end
1654 end
1645
1655
1646 test "#behind_schedule? should be false if the issue has no end_date" do
1656 test "#behind_schedule? should be false if the issue has no end_date" do
1647 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1657 assert !Issue.new(:start_date => 1.day.from_now.to_date,
1648 :due_date => nil,
1658 :due_date => nil,
1649 :done_ratio => 0).behind_schedule?
1659 :done_ratio => 0).behind_schedule?
1650 end
1660 end
1651
1661
1652 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1662 test "#behind_schedule? should be false if the issue has more done than it's calendar time" do
1653 assert !Issue.new(:start_date => 50.days.ago.to_date,
1663 assert !Issue.new(:start_date => 50.days.ago.to_date,
1654 :due_date => 50.days.from_now.to_date,
1664 :due_date => 50.days.from_now.to_date,
1655 :done_ratio => 90).behind_schedule?
1665 :done_ratio => 90).behind_schedule?
1656 end
1666 end
1657
1667
1658 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1668 test "#behind_schedule? should be true if the issue hasn't been started at all" do
1659 assert Issue.new(:start_date => 1.day.ago.to_date,
1669 assert Issue.new(:start_date => 1.day.ago.to_date,
1660 :due_date => 1.day.from_now.to_date,
1670 :due_date => 1.day.from_now.to_date,
1661 :done_ratio => 0).behind_schedule?
1671 :done_ratio => 0).behind_schedule?
1662 end
1672 end
1663
1673
1664 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1674 test "#behind_schedule? should be true if the issue has used more calendar time than it's done ratio" do
1665 assert Issue.new(:start_date => 100.days.ago.to_date,
1675 assert Issue.new(:start_date => 100.days.ago.to_date,
1666 :due_date => Date.today,
1676 :due_date => Date.today,
1667 :done_ratio => 90).behind_schedule?
1677 :done_ratio => 90).behind_schedule?
1668 end
1678 end
1669
1679
1670 test "#assignable_users should be Users" do
1680 test "#assignable_users should be Users" do
1671 assert_kind_of User, Issue.find(1).assignable_users.first
1681 assert_kind_of User, Issue.find(1).assignable_users.first
1672 end
1682 end
1673
1683
1674 test "#assignable_users should include the issue author" do
1684 test "#assignable_users should include the issue author" do
1675 non_project_member = User.generate!
1685 non_project_member = User.generate!
1676 issue = Issue.generate!(:author => non_project_member)
1686 issue = Issue.generate!(:author => non_project_member)
1677
1687
1678 assert issue.assignable_users.include?(non_project_member)
1688 assert issue.assignable_users.include?(non_project_member)
1679 end
1689 end
1680
1690
1681 test "#assignable_users should include the current assignee" do
1691 test "#assignable_users should include the current assignee" do
1682 user = User.generate!
1692 user = User.generate!
1683 issue = Issue.generate!(:assigned_to => user)
1693 issue = Issue.generate!(:assigned_to => user)
1684 user.lock!
1694 user.lock!
1685
1695
1686 assert Issue.find(issue.id).assignable_users.include?(user)
1696 assert Issue.find(issue.id).assignable_users.include?(user)
1687 end
1697 end
1688
1698
1689 test "#assignable_users should not show the issue author twice" do
1699 test "#assignable_users should not show the issue author twice" do
1690 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1700 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
1691 assert_equal 2, assignable_user_ids.length
1701 assert_equal 2, assignable_user_ids.length
1692
1702
1693 assignable_user_ids.each do |user_id|
1703 assignable_user_ids.each do |user_id|
1694 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1704 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length,
1695 "User #{user_id} appears more or less than once"
1705 "User #{user_id} appears more or less than once"
1696 end
1706 end
1697 end
1707 end
1698
1708
1699 test "#assignable_users with issue_group_assignment should include groups" do
1709 test "#assignable_users with issue_group_assignment should include groups" do
1700 issue = Issue.new(:project => Project.find(2))
1710 issue = Issue.new(:project => Project.find(2))
1701
1711
1702 with_settings :issue_group_assignment => '1' do
1712 with_settings :issue_group_assignment => '1' do
1703 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1713 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1704 assert issue.assignable_users.include?(Group.find(11))
1714 assert issue.assignable_users.include?(Group.find(11))
1705 end
1715 end
1706 end
1716 end
1707
1717
1708 test "#assignable_users without issue_group_assignment should not include groups" do
1718 test "#assignable_users without issue_group_assignment should not include groups" do
1709 issue = Issue.new(:project => Project.find(2))
1719 issue = Issue.new(:project => Project.find(2))
1710
1720
1711 with_settings :issue_group_assignment => '0' do
1721 with_settings :issue_group_assignment => '0' do
1712 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1722 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
1713 assert !issue.assignable_users.include?(Group.find(11))
1723 assert !issue.assignable_users.include?(Group.find(11))
1714 end
1724 end
1715 end
1725 end
1716
1726
1717 def test_create_should_send_email_notification
1727 def test_create_should_send_email_notification
1718 ActionMailer::Base.deliveries.clear
1728 ActionMailer::Base.deliveries.clear
1719 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1729 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1720 :author_id => 3, :status_id => 1,
1730 :author_id => 3, :status_id => 1,
1721 :priority => IssuePriority.all.first,
1731 :priority => IssuePriority.all.first,
1722 :subject => 'test_create', :estimated_hours => '1:30')
1732 :subject => 'test_create', :estimated_hours => '1:30')
1723 with_settings :notified_events => %w(issue_added) do
1733 with_settings :notified_events => %w(issue_added) do
1724 assert issue.save
1734 assert issue.save
1725 assert_equal 1, ActionMailer::Base.deliveries.size
1735 assert_equal 1, ActionMailer::Base.deliveries.size
1726 end
1736 end
1727 end
1737 end
1728
1738
1729 def test_create_should_send_one_email_notification_with_both_settings
1739 def test_create_should_send_one_email_notification_with_both_settings
1730 ActionMailer::Base.deliveries.clear
1740 ActionMailer::Base.deliveries.clear
1731 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1741 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1732 :author_id => 3, :status_id => 1,
1742 :author_id => 3, :status_id => 1,
1733 :priority => IssuePriority.all.first,
1743 :priority => IssuePriority.all.first,
1734 :subject => 'test_create', :estimated_hours => '1:30')
1744 :subject => 'test_create', :estimated_hours => '1:30')
1735 with_settings :notified_events => %w(issue_added issue_updated) do
1745 with_settings :notified_events => %w(issue_added issue_updated) do
1736 assert issue.save
1746 assert issue.save
1737 assert_equal 1, ActionMailer::Base.deliveries.size
1747 assert_equal 1, ActionMailer::Base.deliveries.size
1738 end
1748 end
1739 end
1749 end
1740
1750
1741 def test_create_should_not_send_email_notification_with_no_setting
1751 def test_create_should_not_send_email_notification_with_no_setting
1742 ActionMailer::Base.deliveries.clear
1752 ActionMailer::Base.deliveries.clear
1743 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1753 issue = Issue.new(:project_id => 1, :tracker_id => 1,
1744 :author_id => 3, :status_id => 1,
1754 :author_id => 3, :status_id => 1,
1745 :priority => IssuePriority.all.first,
1755 :priority => IssuePriority.all.first,
1746 :subject => 'test_create', :estimated_hours => '1:30')
1756 :subject => 'test_create', :estimated_hours => '1:30')
1747 with_settings :notified_events => [] do
1757 with_settings :notified_events => [] do
1748 assert issue.save
1758 assert issue.save
1749 assert_equal 0, ActionMailer::Base.deliveries.size
1759 assert_equal 0, ActionMailer::Base.deliveries.size
1750 end
1760 end
1751 end
1761 end
1752
1762
1753 def test_update_should_notify_previous_assignee
1763 def test_update_should_notify_previous_assignee
1754 ActionMailer::Base.deliveries.clear
1764 ActionMailer::Base.deliveries.clear
1755 user = User.find(3)
1765 user = User.find(3)
1756 user.members.update_all ["mail_notification = ?", false]
1766 user.members.update_all ["mail_notification = ?", false]
1757 user.update_attribute :mail_notification, 'only_assigned'
1767 user.update_attribute :mail_notification, 'only_assigned'
1758
1768
1759 issue = Issue.find(2)
1769 issue = Issue.find(2)
1760 issue.init_journal User.find(1)
1770 issue.init_journal User.find(1)
1761 issue.assigned_to = nil
1771 issue.assigned_to = nil
1762 issue.save!
1772 issue.save!
1763 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
1773 assert_include user.mail, ActionMailer::Base.deliveries.last.bcc
1764 end
1774 end
1765
1775
1766 def test_stale_issue_should_not_send_email_notification
1776 def test_stale_issue_should_not_send_email_notification
1767 ActionMailer::Base.deliveries.clear
1777 ActionMailer::Base.deliveries.clear
1768 issue = Issue.find(1)
1778 issue = Issue.find(1)
1769 stale = Issue.find(1)
1779 stale = Issue.find(1)
1770
1780
1771 issue.init_journal(User.find(1))
1781 issue.init_journal(User.find(1))
1772 issue.subject = 'Subjet update'
1782 issue.subject = 'Subjet update'
1773 with_settings :notified_events => %w(issue_updated) do
1783 with_settings :notified_events => %w(issue_updated) do
1774 assert issue.save
1784 assert issue.save
1775 assert_equal 1, ActionMailer::Base.deliveries.size
1785 assert_equal 1, ActionMailer::Base.deliveries.size
1776 ActionMailer::Base.deliveries.clear
1786 ActionMailer::Base.deliveries.clear
1777
1787
1778 stale.init_journal(User.find(1))
1788 stale.init_journal(User.find(1))
1779 stale.subject = 'Another subjet update'
1789 stale.subject = 'Another subjet update'
1780 assert_raise ActiveRecord::StaleObjectError do
1790 assert_raise ActiveRecord::StaleObjectError do
1781 stale.save
1791 stale.save
1782 end
1792 end
1783 assert ActionMailer::Base.deliveries.empty?
1793 assert ActionMailer::Base.deliveries.empty?
1784 end
1794 end
1785 end
1795 end
1786
1796
1787 def test_journalized_description
1797 def test_journalized_description
1788 IssueCustomField.delete_all
1798 IssueCustomField.delete_all
1789
1799
1790 i = Issue.first
1800 i = Issue.first
1791 old_description = i.description
1801 old_description = i.description
1792 new_description = "This is the new description"
1802 new_description = "This is the new description"
1793
1803
1794 i.init_journal(User.find(2))
1804 i.init_journal(User.find(2))
1795 i.description = new_description
1805 i.description = new_description
1796 assert_difference 'Journal.count', 1 do
1806 assert_difference 'Journal.count', 1 do
1797 assert_difference 'JournalDetail.count', 1 do
1807 assert_difference 'JournalDetail.count', 1 do
1798 i.save!
1808 i.save!
1799 end
1809 end
1800 end
1810 end
1801
1811
1802 detail = JournalDetail.order('id DESC').first
1812 detail = JournalDetail.order('id DESC').first
1803 assert_equal i, detail.journal.journalized
1813 assert_equal i, detail.journal.journalized
1804 assert_equal 'attr', detail.property
1814 assert_equal 'attr', detail.property
1805 assert_equal 'description', detail.prop_key
1815 assert_equal 'description', detail.prop_key
1806 assert_equal old_description, detail.old_value
1816 assert_equal old_description, detail.old_value
1807 assert_equal new_description, detail.value
1817 assert_equal new_description, detail.value
1808 end
1818 end
1809
1819
1810 def test_blank_descriptions_should_not_be_journalized
1820 def test_blank_descriptions_should_not_be_journalized
1811 IssueCustomField.delete_all
1821 IssueCustomField.delete_all
1812 Issue.where(:id => 1).update_all("description = NULL")
1822 Issue.where(:id => 1).update_all("description = NULL")
1813
1823
1814 i = Issue.find(1)
1824 i = Issue.find(1)
1815 i.init_journal(User.find(2))
1825 i.init_journal(User.find(2))
1816 i.subject = "blank description"
1826 i.subject = "blank description"
1817 i.description = "\r\n"
1827 i.description = "\r\n"
1818
1828
1819 assert_difference 'Journal.count', 1 do
1829 assert_difference 'Journal.count', 1 do
1820 assert_difference 'JournalDetail.count', 1 do
1830 assert_difference 'JournalDetail.count', 1 do
1821 i.save!
1831 i.save!
1822 end
1832 end
1823 end
1833 end
1824 end
1834 end
1825
1835
1826 def test_journalized_multi_custom_field
1836 def test_journalized_multi_custom_field
1827 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1837 field = IssueCustomField.create!(:name => 'filter', :field_format => 'list',
1828 :is_filter => true, :is_for_all => true,
1838 :is_filter => true, :is_for_all => true,
1829 :tracker_ids => [1],
1839 :tracker_ids => [1],
1830 :possible_values => ['value1', 'value2', 'value3'],
1840 :possible_values => ['value1', 'value2', 'value3'],
1831 :multiple => true)
1841 :multiple => true)
1832
1842
1833 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1843 issue = Issue.create!(:project_id => 1, :tracker_id => 1,
1834 :subject => 'Test', :author_id => 1)
1844 :subject => 'Test', :author_id => 1)
1835
1845
1836 assert_difference 'Journal.count' do
1846 assert_difference 'Journal.count' do
1837 assert_difference 'JournalDetail.count' do
1847 assert_difference 'JournalDetail.count' do
1838 issue.init_journal(User.first)
1848 issue.init_journal(User.first)
1839 issue.custom_field_values = {field.id => ['value1']}
1849 issue.custom_field_values = {field.id => ['value1']}
1840 issue.save!
1850 issue.save!
1841 end
1851 end
1842 assert_difference 'JournalDetail.count' do
1852 assert_difference 'JournalDetail.count' do
1843 issue.init_journal(User.first)
1853 issue.init_journal(User.first)
1844 issue.custom_field_values = {field.id => ['value1', 'value2']}
1854 issue.custom_field_values = {field.id => ['value1', 'value2']}
1845 issue.save!
1855 issue.save!
1846 end
1856 end
1847 assert_difference 'JournalDetail.count', 2 do
1857 assert_difference 'JournalDetail.count', 2 do
1848 issue.init_journal(User.first)
1858 issue.init_journal(User.first)
1849 issue.custom_field_values = {field.id => ['value3', 'value2']}
1859 issue.custom_field_values = {field.id => ['value3', 'value2']}
1850 issue.save!
1860 issue.save!
1851 end
1861 end
1852 assert_difference 'JournalDetail.count', 2 do
1862 assert_difference 'JournalDetail.count', 2 do
1853 issue.init_journal(User.first)
1863 issue.init_journal(User.first)
1854 issue.custom_field_values = {field.id => nil}
1864 issue.custom_field_values = {field.id => nil}
1855 issue.save!
1865 issue.save!
1856 end
1866 end
1857 end
1867 end
1858 end
1868 end
1859
1869
1860 def test_description_eol_should_be_normalized
1870 def test_description_eol_should_be_normalized
1861 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1871 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
1862 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1872 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
1863 end
1873 end
1864
1874
1865 def test_saving_twice_should_not_duplicate_journal_details
1875 def test_saving_twice_should_not_duplicate_journal_details
1866 i = Issue.first
1876 i = Issue.first
1867 i.init_journal(User.find(2), 'Some notes')
1877 i.init_journal(User.find(2), 'Some notes')
1868 # initial changes
1878 # initial changes
1869 i.subject = 'New subject'
1879 i.subject = 'New subject'
1870 i.done_ratio = i.done_ratio + 10
1880 i.done_ratio = i.done_ratio + 10
1871 assert_difference 'Journal.count' do
1881 assert_difference 'Journal.count' do
1872 assert i.save
1882 assert i.save
1873 end
1883 end
1874 # 1 more change
1884 # 1 more change
1875 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1885 i.priority = IssuePriority.where("id <> ?", i.priority_id).first
1876 assert_no_difference 'Journal.count' do
1886 assert_no_difference 'Journal.count' do
1877 assert_difference 'JournalDetail.count', 1 do
1887 assert_difference 'JournalDetail.count', 1 do
1878 i.save
1888 i.save
1879 end
1889 end
1880 end
1890 end
1881 # no more change
1891 # no more change
1882 assert_no_difference 'Journal.count' do
1892 assert_no_difference 'Journal.count' do
1883 assert_no_difference 'JournalDetail.count' do
1893 assert_no_difference 'JournalDetail.count' do
1884 i.save
1894 i.save
1885 end
1895 end
1886 end
1896 end
1887 end
1897 end
1888
1898
1889 def test_all_dependent_issues
1899 def test_all_dependent_issues
1890 IssueRelation.delete_all
1900 IssueRelation.delete_all
1891 assert IssueRelation.create!(:issue_from => Issue.find(1),
1901 assert IssueRelation.create!(:issue_from => Issue.find(1),
1892 :issue_to => Issue.find(2),
1902 :issue_to => Issue.find(2),
1893 :relation_type => IssueRelation::TYPE_PRECEDES)
1903 :relation_type => IssueRelation::TYPE_PRECEDES)
1894 assert IssueRelation.create!(:issue_from => Issue.find(2),
1904 assert IssueRelation.create!(:issue_from => Issue.find(2),
1895 :issue_to => Issue.find(3),
1905 :issue_to => Issue.find(3),
1896 :relation_type => IssueRelation::TYPE_PRECEDES)
1906 :relation_type => IssueRelation::TYPE_PRECEDES)
1897 assert IssueRelation.create!(:issue_from => Issue.find(3),
1907 assert IssueRelation.create!(:issue_from => Issue.find(3),
1898 :issue_to => Issue.find(8),
1908 :issue_to => Issue.find(8),
1899 :relation_type => IssueRelation::TYPE_PRECEDES)
1909 :relation_type => IssueRelation::TYPE_PRECEDES)
1900
1910
1901 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1911 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
1902 end
1912 end
1903
1913
1904 def test_all_dependent_issues_with_subtask
1914 def test_all_dependent_issues_with_subtask
1905 IssueRelation.delete_all
1915 IssueRelation.delete_all
1906
1916
1907 project = Project.generate!(:name => "testproject")
1917 project = Project.generate!(:name => "testproject")
1908
1918
1909 parentIssue = Issue.generate!(:project => project)
1919 parentIssue = Issue.generate!(:project => project)
1910 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1920 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1911 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1921 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1912
1922
1913 assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort
1923 assert_equal [childIssue1.id, childIssue2.id].sort, parentIssue.all_dependent_issues.collect(&:id).uniq.sort
1914 end
1924 end
1915
1925
1916 def test_all_dependent_issues_does_not_include_self
1926 def test_all_dependent_issues_does_not_include_self
1917 IssueRelation.delete_all
1927 IssueRelation.delete_all
1918
1928
1919 project = Project.generate!(:name => "testproject")
1929 project = Project.generate!(:name => "testproject")
1920
1930
1921 parentIssue = Issue.generate!(:project => project)
1931 parentIssue = Issue.generate!(:project => project)
1922 childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1932 childIssue = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1923
1933
1924 assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id)
1934 assert_equal [childIssue.id], parentIssue.all_dependent_issues.collect(&:id)
1925 end
1935 end
1926
1936
1927 def test_all_dependent_issues_with_parenttask_and_sibling
1937 def test_all_dependent_issues_with_parenttask_and_sibling
1928 IssueRelation.delete_all
1938 IssueRelation.delete_all
1929
1939
1930 project = Project.generate!(:name => "testproject")
1940 project = Project.generate!(:name => "testproject")
1931
1941
1932 parentIssue = Issue.generate!(:project => project)
1942 parentIssue = Issue.generate!(:project => project)
1933 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1943 childIssue1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1934 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1944 childIssue2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue.id)
1935
1945
1936 assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id)
1946 assert_equal [parentIssue.id].sort, childIssue1.all_dependent_issues.collect(&:id)
1937 end
1947 end
1938
1948
1939 def test_all_dependent_issues_with_relation_to_leaf_in_other_tree
1949 def test_all_dependent_issues_with_relation_to_leaf_in_other_tree
1940 IssueRelation.delete_all
1950 IssueRelation.delete_all
1941
1951
1942 project = Project.generate!(:name => "testproject")
1952 project = Project.generate!(:name => "testproject")
1943
1953
1944 parentIssue1 = Issue.generate!(:project => project)
1954 parentIssue1 = Issue.generate!(:project => project)
1945 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1955 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1946 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1956 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1947
1957
1948 parentIssue2 = Issue.generate!(:project => project)
1958 parentIssue2 = Issue.generate!(:project => project)
1949 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1959 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1950 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1960 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1951
1961
1952
1962
1953 assert IssueRelation.create(:issue_from => parentIssue1,
1963 assert IssueRelation.create(:issue_from => parentIssue1,
1954 :issue_to => childIssue2_2,
1964 :issue_to => childIssue2_2,
1955 :relation_type => IssueRelation::TYPE_BLOCKS)
1965 :relation_type => IssueRelation::TYPE_BLOCKS)
1956
1966
1957 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort,
1967 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_2.id].sort,
1958 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1968 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1959 end
1969 end
1960
1970
1961 def test_all_dependent_issues_with_relation_to_parent_in_other_tree
1971 def test_all_dependent_issues_with_relation_to_parent_in_other_tree
1962 IssueRelation.delete_all
1972 IssueRelation.delete_all
1963
1973
1964 project = Project.generate!(:name => "testproject")
1974 project = Project.generate!(:name => "testproject")
1965
1975
1966 parentIssue1 = Issue.generate!(:project => project)
1976 parentIssue1 = Issue.generate!(:project => project)
1967 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1977 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1968 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1978 childIssue1_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1969
1979
1970 parentIssue2 = Issue.generate!(:project => project)
1980 parentIssue2 = Issue.generate!(:project => project)
1971 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1981 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1972 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1982 childIssue2_2 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1973
1983
1974
1984
1975 assert IssueRelation.create(:issue_from => parentIssue1,
1985 assert IssueRelation.create(:issue_from => parentIssue1,
1976 :issue_to => parentIssue2,
1986 :issue_to => parentIssue2,
1977 :relation_type => IssueRelation::TYPE_BLOCKS)
1987 :relation_type => IssueRelation::TYPE_BLOCKS)
1978
1988
1979 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort,
1989 assert_equal [childIssue1_1.id, childIssue1_2.id, parentIssue2.id, childIssue2_1.id, childIssue2_2.id].sort,
1980 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1990 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
1981 end
1991 end
1982
1992
1983 def test_all_dependent_issues_with_transitive_relation
1993 def test_all_dependent_issues_with_transitive_relation
1984 IssueRelation.delete_all
1994 IssueRelation.delete_all
1985
1995
1986 project = Project.generate!(:name => "testproject")
1996 project = Project.generate!(:name => "testproject")
1987
1997
1988 parentIssue1 = Issue.generate!(:project => project)
1998 parentIssue1 = Issue.generate!(:project => project)
1989 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1999 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
1990
2000
1991 parentIssue2 = Issue.generate!(:project => project)
2001 parentIssue2 = Issue.generate!(:project => project)
1992 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2002 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
1993
2003
1994 independentIssue = Issue.generate!(:project => project)
2004 independentIssue = Issue.generate!(:project => project)
1995
2005
1996 assert IssueRelation.create(:issue_from => parentIssue1,
2006 assert IssueRelation.create(:issue_from => parentIssue1,
1997 :issue_to => childIssue2_1,
2007 :issue_to => childIssue2_1,
1998 :relation_type => IssueRelation::TYPE_RELATES)
2008 :relation_type => IssueRelation::TYPE_RELATES)
1999
2009
2000 assert IssueRelation.create(:issue_from => childIssue2_1,
2010 assert IssueRelation.create(:issue_from => childIssue2_1,
2001 :issue_to => independentIssue,
2011 :issue_to => independentIssue,
2002 :relation_type => IssueRelation::TYPE_RELATES)
2012 :relation_type => IssueRelation::TYPE_RELATES)
2003
2013
2004 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2014 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2005 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2015 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2006 end
2016 end
2007
2017
2008 def test_all_dependent_issues_with_transitive_relation2
2018 def test_all_dependent_issues_with_transitive_relation2
2009 IssueRelation.delete_all
2019 IssueRelation.delete_all
2010
2020
2011 project = Project.generate!(:name => "testproject")
2021 project = Project.generate!(:name => "testproject")
2012
2022
2013 parentIssue1 = Issue.generate!(:project => project)
2023 parentIssue1 = Issue.generate!(:project => project)
2014 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2024 childIssue1_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue1.id)
2015
2025
2016 parentIssue2 = Issue.generate!(:project => project)
2026 parentIssue2 = Issue.generate!(:project => project)
2017 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2027 childIssue2_1 = Issue.generate!(:project => project, :parent_issue_id => parentIssue2.id)
2018
2028
2019 independentIssue = Issue.generate!(:project => project)
2029 independentIssue = Issue.generate!(:project => project)
2020
2030
2021 assert IssueRelation.create(:issue_from => parentIssue1,
2031 assert IssueRelation.create(:issue_from => parentIssue1,
2022 :issue_to => independentIssue,
2032 :issue_to => independentIssue,
2023 :relation_type => IssueRelation::TYPE_RELATES)
2033 :relation_type => IssueRelation::TYPE_RELATES)
2024
2034
2025 assert IssueRelation.create(:issue_from => independentIssue,
2035 assert IssueRelation.create(:issue_from => independentIssue,
2026 :issue_to => childIssue2_1,
2036 :issue_to => childIssue2_1,
2027 :relation_type => IssueRelation::TYPE_RELATES)
2037 :relation_type => IssueRelation::TYPE_RELATES)
2028
2038
2029 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2039 assert_equal [childIssue1_1.id, parentIssue2.id, childIssue2_1.id, independentIssue.id].sort,
2030 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2040 parentIssue1.all_dependent_issues.collect(&:id).uniq.sort
2031
2041
2032 end
2042 end
2033
2043
2034 def test_all_dependent_issues_with_persistent_circular_dependency
2044 def test_all_dependent_issues_with_persistent_circular_dependency
2035 IssueRelation.delete_all
2045 IssueRelation.delete_all
2036 assert IssueRelation.create!(:issue_from => Issue.find(1),
2046 assert IssueRelation.create!(:issue_from => Issue.find(1),
2037 :issue_to => Issue.find(2),
2047 :issue_to => Issue.find(2),
2038 :relation_type => IssueRelation::TYPE_PRECEDES)
2048 :relation_type => IssueRelation::TYPE_PRECEDES)
2039 assert IssueRelation.create!(:issue_from => Issue.find(2),
2049 assert IssueRelation.create!(:issue_from => Issue.find(2),
2040 :issue_to => Issue.find(3),
2050 :issue_to => Issue.find(3),
2041 :relation_type => IssueRelation::TYPE_PRECEDES)
2051 :relation_type => IssueRelation::TYPE_PRECEDES)
2042
2052
2043 r = IssueRelation.create!(:issue_from => Issue.find(3),
2053 r = IssueRelation.create!(:issue_from => Issue.find(3),
2044 :issue_to => Issue.find(7),
2054 :issue_to => Issue.find(7),
2045 :relation_type => IssueRelation::TYPE_PRECEDES)
2055 :relation_type => IssueRelation::TYPE_PRECEDES)
2046 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2056 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2047
2057
2048 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
2058 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
2049 end
2059 end
2050
2060
2051 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
2061 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
2052 IssueRelation.delete_all
2062 IssueRelation.delete_all
2053 assert IssueRelation.create!(:issue_from => Issue.find(1),
2063 assert IssueRelation.create!(:issue_from => Issue.find(1),
2054 :issue_to => Issue.find(2),
2064 :issue_to => Issue.find(2),
2055 :relation_type => IssueRelation::TYPE_RELATES)
2065 :relation_type => IssueRelation::TYPE_RELATES)
2056 assert IssueRelation.create!(:issue_from => Issue.find(2),
2066 assert IssueRelation.create!(:issue_from => Issue.find(2),
2057 :issue_to => Issue.find(3),
2067 :issue_to => Issue.find(3),
2058 :relation_type => IssueRelation::TYPE_RELATES)
2068 :relation_type => IssueRelation::TYPE_RELATES)
2059 assert IssueRelation.create!(:issue_from => Issue.find(3),
2069 assert IssueRelation.create!(:issue_from => Issue.find(3),
2060 :issue_to => Issue.find(8),
2070 :issue_to => Issue.find(8),
2061 :relation_type => IssueRelation::TYPE_RELATES)
2071 :relation_type => IssueRelation::TYPE_RELATES)
2062
2072
2063 r = IssueRelation.create!(:issue_from => Issue.find(8),
2073 r = IssueRelation.create!(:issue_from => Issue.find(8),
2064 :issue_to => Issue.find(7),
2074 :issue_to => Issue.find(7),
2065 :relation_type => IssueRelation::TYPE_RELATES)
2075 :relation_type => IssueRelation::TYPE_RELATES)
2066 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 2")
2076 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 2")
2067
2077
2068 r = IssueRelation.create!(:issue_from => Issue.find(3),
2078 r = IssueRelation.create!(:issue_from => Issue.find(3),
2069 :issue_to => Issue.find(7),
2079 :issue_to => Issue.find(7),
2070 :relation_type => IssueRelation::TYPE_RELATES)
2080 :relation_type => IssueRelation::TYPE_RELATES)
2071 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2081 IssueRelation.where(["id = ?", r.id]).update_all("issue_to_id = 1")
2072
2082
2073 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
2083 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
2074 end
2084 end
2075
2085
2076 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2086 test "#done_ratio should use the issue_status according to Setting.issue_done_ratio" do
2077 @issue = Issue.find(1)
2087 @issue = Issue.find(1)
2078 @issue_status = IssueStatus.find(1)
2088 @issue_status = IssueStatus.find(1)
2079 @issue_status.update_attribute(:default_done_ratio, 50)
2089 @issue_status.update_attribute(:default_done_ratio, 50)
2080 @issue2 = Issue.find(2)
2090 @issue2 = Issue.find(2)
2081 @issue_status2 = IssueStatus.find(2)
2091 @issue_status2 = IssueStatus.find(2)
2082 @issue_status2.update_attribute(:default_done_ratio, 0)
2092 @issue_status2.update_attribute(:default_done_ratio, 0)
2083
2093
2084 with_settings :issue_done_ratio => 'issue_field' do
2094 with_settings :issue_done_ratio => 'issue_field' do
2085 assert_equal 0, @issue.done_ratio
2095 assert_equal 0, @issue.done_ratio
2086 assert_equal 30, @issue2.done_ratio
2096 assert_equal 30, @issue2.done_ratio
2087 end
2097 end
2088
2098
2089 with_settings :issue_done_ratio => 'issue_status' do
2099 with_settings :issue_done_ratio => 'issue_status' do
2090 assert_equal 50, @issue.done_ratio
2100 assert_equal 50, @issue.done_ratio
2091 assert_equal 0, @issue2.done_ratio
2101 assert_equal 0, @issue2.done_ratio
2092 end
2102 end
2093 end
2103 end
2094
2104
2095 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2105 test "#update_done_ratio_from_issue_status should update done_ratio according to Setting.issue_done_ratio" do
2096 @issue = Issue.find(1)
2106 @issue = Issue.find(1)
2097 @issue_status = IssueStatus.find(1)
2107 @issue_status = IssueStatus.find(1)
2098 @issue_status.update_attribute(:default_done_ratio, 50)
2108 @issue_status.update_attribute(:default_done_ratio, 50)
2099 @issue2 = Issue.find(2)
2109 @issue2 = Issue.find(2)
2100 @issue_status2 = IssueStatus.find(2)
2110 @issue_status2 = IssueStatus.find(2)
2101 @issue_status2.update_attribute(:default_done_ratio, 0)
2111 @issue_status2.update_attribute(:default_done_ratio, 0)
2102
2112
2103 with_settings :issue_done_ratio => 'issue_field' do
2113 with_settings :issue_done_ratio => 'issue_field' do
2104 @issue.update_done_ratio_from_issue_status
2114 @issue.update_done_ratio_from_issue_status
2105 @issue2.update_done_ratio_from_issue_status
2115 @issue2.update_done_ratio_from_issue_status
2106
2116
2107 assert_equal 0, @issue.read_attribute(:done_ratio)
2117 assert_equal 0, @issue.read_attribute(:done_ratio)
2108 assert_equal 30, @issue2.read_attribute(:done_ratio)
2118 assert_equal 30, @issue2.read_attribute(:done_ratio)
2109 end
2119 end
2110
2120
2111 with_settings :issue_done_ratio => 'issue_status' do
2121 with_settings :issue_done_ratio => 'issue_status' do
2112 @issue.update_done_ratio_from_issue_status
2122 @issue.update_done_ratio_from_issue_status
2113 @issue2.update_done_ratio_from_issue_status
2123 @issue2.update_done_ratio_from_issue_status
2114
2124
2115 assert_equal 50, @issue.read_attribute(:done_ratio)
2125 assert_equal 50, @issue.read_attribute(:done_ratio)
2116 assert_equal 0, @issue2.read_attribute(:done_ratio)
2126 assert_equal 0, @issue2.read_attribute(:done_ratio)
2117 end
2127 end
2118 end
2128 end
2119
2129
2120 test "#by_tracker" do
2130 test "#by_tracker" do
2121 User.current = User.anonymous
2131 User.current = User.anonymous
2122 groups = Issue.by_tracker(Project.find(1))
2132 groups = Issue.by_tracker(Project.find(1))
2123 assert_equal 3, groups.count
2133 assert_equal 3, groups.count
2124 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2134 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2125 end
2135 end
2126
2136
2127 test "#by_version" do
2137 test "#by_version" do
2128 User.current = User.anonymous
2138 User.current = User.anonymous
2129 groups = Issue.by_version(Project.find(1))
2139 groups = Issue.by_version(Project.find(1))
2130 assert_equal 3, groups.count
2140 assert_equal 3, groups.count
2131 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2141 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2132 end
2142 end
2133
2143
2134 test "#by_priority" do
2144 test "#by_priority" do
2135 User.current = User.anonymous
2145 User.current = User.anonymous
2136 groups = Issue.by_priority(Project.find(1))
2146 groups = Issue.by_priority(Project.find(1))
2137 assert_equal 4, groups.count
2147 assert_equal 4, groups.count
2138 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2148 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2139 end
2149 end
2140
2150
2141 test "#by_category" do
2151 test "#by_category" do
2142 User.current = User.anonymous
2152 User.current = User.anonymous
2143 groups = Issue.by_category(Project.find(1))
2153 groups = Issue.by_category(Project.find(1))
2144 assert_equal 2, groups.count
2154 assert_equal 2, groups.count
2145 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2155 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2146 end
2156 end
2147
2157
2148 test "#by_assigned_to" do
2158 test "#by_assigned_to" do
2149 User.current = User.anonymous
2159 User.current = User.anonymous
2150 groups = Issue.by_assigned_to(Project.find(1))
2160 groups = Issue.by_assigned_to(Project.find(1))
2151 assert_equal 2, groups.count
2161 assert_equal 2, groups.count
2152 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2162 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2153 end
2163 end
2154
2164
2155 test "#by_author" do
2165 test "#by_author" do
2156 User.current = User.anonymous
2166 User.current = User.anonymous
2157 groups = Issue.by_author(Project.find(1))
2167 groups = Issue.by_author(Project.find(1))
2158 assert_equal 4, groups.count
2168 assert_equal 4, groups.count
2159 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2169 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2160 end
2170 end
2161
2171
2162 test "#by_subproject" do
2172 test "#by_subproject" do
2163 User.current = User.anonymous
2173 User.current = User.anonymous
2164 groups = Issue.by_subproject(Project.find(1))
2174 groups = Issue.by_subproject(Project.find(1))
2165 # Private descendant not visible
2175 # Private descendant not visible
2166 assert_equal 1, groups.count
2176 assert_equal 1, groups.count
2167 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2177 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
2168 end
2178 end
2169
2179
2170 def test_recently_updated_scope
2180 def test_recently_updated_scope
2171 #should return the last updated issue
2181 #should return the last updated issue
2172 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2182 assert_equal Issue.reorder("updated_on DESC").first, Issue.recently_updated.limit(1).first
2173 end
2183 end
2174
2184
2175 def test_on_active_projects_scope
2185 def test_on_active_projects_scope
2176 assert Project.find(2).archive
2186 assert Project.find(2).archive
2177
2187
2178 before = Issue.on_active_project.length
2188 before = Issue.on_active_project.length
2179 # test inclusion to results
2189 # test inclusion to results
2180 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2190 issue = Issue.generate!(:tracker => Project.find(2).trackers.first)
2181 assert_equal before + 1, Issue.on_active_project.length
2191 assert_equal before + 1, Issue.on_active_project.length
2182
2192
2183 # Move to an archived project
2193 # Move to an archived project
2184 issue.project = Project.find(2)
2194 issue.project = Project.find(2)
2185 assert issue.save
2195 assert issue.save
2186 assert_equal before, Issue.on_active_project.length
2196 assert_equal before, Issue.on_active_project.length
2187 end
2197 end
2188
2198
2189 test "Issue#recipients should include project recipients" do
2199 test "Issue#recipients should include project recipients" do
2190 issue = Issue.generate!
2200 issue = Issue.generate!
2191 assert issue.project.recipients.present?
2201 assert issue.project.recipients.present?
2192 issue.project.recipients.each do |project_recipient|
2202 issue.project.recipients.each do |project_recipient|
2193 assert issue.recipients.include?(project_recipient)
2203 assert issue.recipients.include?(project_recipient)
2194 end
2204 end
2195 end
2205 end
2196
2206
2197 test "Issue#recipients should include the author if the author is active" do
2207 test "Issue#recipients should include the author if the author is active" do
2198 issue = Issue.generate!(:author => User.generate!)
2208 issue = Issue.generate!(:author => User.generate!)
2199 assert issue.author, "No author set for Issue"
2209 assert issue.author, "No author set for Issue"
2200 assert issue.recipients.include?(issue.author.mail)
2210 assert issue.recipients.include?(issue.author.mail)
2201 end
2211 end
2202
2212
2203 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2213 test "Issue#recipients should include the assigned to user if the assigned to user is active" do
2204 issue = Issue.generate!(:assigned_to => User.generate!)
2214 issue = Issue.generate!(:assigned_to => User.generate!)
2205 assert issue.assigned_to, "No assigned_to set for Issue"
2215 assert issue.assigned_to, "No assigned_to set for Issue"
2206 assert issue.recipients.include?(issue.assigned_to.mail)
2216 assert issue.recipients.include?(issue.assigned_to.mail)
2207 end
2217 end
2208
2218
2209 test "Issue#recipients should not include users who opt out of all email" do
2219 test "Issue#recipients should not include users who opt out of all email" do
2210 issue = Issue.generate!(:author => User.generate!)
2220 issue = Issue.generate!(:author => User.generate!)
2211 issue.author.update_attribute(:mail_notification, :none)
2221 issue.author.update_attribute(:mail_notification, :none)
2212 assert !issue.recipients.include?(issue.author.mail)
2222 assert !issue.recipients.include?(issue.author.mail)
2213 end
2223 end
2214
2224
2215 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2225 test "Issue#recipients should not include the issue author if they are only notified of assigned issues" do
2216 issue = Issue.generate!(:author => User.generate!)
2226 issue = Issue.generate!(:author => User.generate!)
2217 issue.author.update_attribute(:mail_notification, :only_assigned)
2227 issue.author.update_attribute(:mail_notification, :only_assigned)
2218 assert !issue.recipients.include?(issue.author.mail)
2228 assert !issue.recipients.include?(issue.author.mail)
2219 end
2229 end
2220
2230
2221 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2231 test "Issue#recipients should not include the assigned user if they are only notified of owned issues" do
2222 issue = Issue.generate!(:assigned_to => User.generate!)
2232 issue = Issue.generate!(:assigned_to => User.generate!)
2223 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2233 issue.assigned_to.update_attribute(:mail_notification, :only_owner)
2224 assert !issue.recipients.include?(issue.assigned_to.mail)
2234 assert !issue.recipients.include?(issue.assigned_to.mail)
2225 end
2235 end
2226
2236
2227 def test_last_journal_id_with_journals_should_return_the_journal_id
2237 def test_last_journal_id_with_journals_should_return_the_journal_id
2228 assert_equal 2, Issue.find(1).last_journal_id
2238 assert_equal 2, Issue.find(1).last_journal_id
2229 end
2239 end
2230
2240
2231 def test_last_journal_id_without_journals_should_return_nil
2241 def test_last_journal_id_without_journals_should_return_nil
2232 assert_nil Issue.find(3).last_journal_id
2242 assert_nil Issue.find(3).last_journal_id
2233 end
2243 end
2234
2244
2235 def test_journals_after_should_return_journals_with_greater_id
2245 def test_journals_after_should_return_journals_with_greater_id
2236 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2246 assert_equal [Journal.find(2)], Issue.find(1).journals_after('1')
2237 assert_equal [], Issue.find(1).journals_after('2')
2247 assert_equal [], Issue.find(1).journals_after('2')
2238 end
2248 end
2239
2249
2240 def test_journals_after_with_blank_arg_should_return_all_journals
2250 def test_journals_after_with_blank_arg_should_return_all_journals
2241 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2251 assert_equal [Journal.find(1), Journal.find(2)], Issue.find(1).journals_after('')
2242 end
2252 end
2243
2253
2244 def test_css_classes_should_include_tracker
2254 def test_css_classes_should_include_tracker
2245 issue = Issue.new(:tracker => Tracker.find(2))
2255 issue = Issue.new(:tracker => Tracker.find(2))
2246 classes = issue.css_classes.split(' ')
2256 classes = issue.css_classes.split(' ')
2247 assert_include 'tracker-2', classes
2257 assert_include 'tracker-2', classes
2248 end
2258 end
2249
2259
2250 def test_css_classes_should_include_priority
2260 def test_css_classes_should_include_priority
2251 issue = Issue.new(:priority => IssuePriority.find(8))
2261 issue = Issue.new(:priority => IssuePriority.find(8))
2252 classes = issue.css_classes.split(' ')
2262 classes = issue.css_classes.split(' ')
2253 assert_include 'priority-8', classes
2263 assert_include 'priority-8', classes
2254 assert_include 'priority-highest', classes
2264 assert_include 'priority-highest', classes
2255 end
2265 end
2256
2266
2257 def test_css_classes_should_include_user_and_group_assignment
2267 def test_css_classes_should_include_user_and_group_assignment
2258 project = Project.first
2268 project = Project.first
2259 user = User.generate!
2269 user = User.generate!
2260 group = Group.generate!
2270 group = Group.generate!
2261 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2271 Member.create!(:principal => group, :project => project, :role_ids => [1, 2])
2262 group.users << user
2272 group.users << user
2263 assert user.member_of?(project)
2273 assert user.member_of?(project)
2264 issue1 = Issue.generate(:assigned_to_id => group.id)
2274 issue1 = Issue.generate(:assigned_to_id => group.id)
2265 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2275 assert_include 'assigned-to-my-group', issue1.css_classes(user)
2266 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2276 assert_not_include 'assigned-to-me', issue1.css_classes(user)
2267 issue2 = Issue.generate(:assigned_to_id => user.id)
2277 issue2 = Issue.generate(:assigned_to_id => user.id)
2268 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2278 assert_not_include 'assigned-to-my-group', issue2.css_classes(user)
2269 assert_include 'assigned-to-me', issue2.css_classes(user)
2279 assert_include 'assigned-to-me', issue2.css_classes(user)
2270 end
2280 end
2271
2281
2272 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2282 def test_save_attachments_with_hash_should_save_attachments_in_keys_order
2273 set_tmp_attachments_directory
2283 set_tmp_attachments_directory
2274 issue = Issue.generate!
2284 issue = Issue.generate!
2275 issue.save_attachments({
2285 issue.save_attachments({
2276 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2286 'p0' => {'file' => mock_file_with_options(:original_filename => 'upload')},
2277 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2287 '3' => {'file' => mock_file_with_options(:original_filename => 'bar')},
2278 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2288 '1' => {'file' => mock_file_with_options(:original_filename => 'foo')}
2279 })
2289 })
2280 issue.attach_saved_attachments
2290 issue.attach_saved_attachments
2281
2291
2282 assert_equal 3, issue.reload.attachments.count
2292 assert_equal 3, issue.reload.attachments.count
2283 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2293 assert_equal %w(upload foo bar), issue.attachments.map(&:filename)
2284 end
2294 end
2285
2295
2286 def test_closed_on_should_be_nil_when_creating_an_open_issue
2296 def test_closed_on_should_be_nil_when_creating_an_open_issue
2287 issue = Issue.generate!(:status_id => 1).reload
2297 issue = Issue.generate!(:status_id => 1).reload
2288 assert !issue.closed?
2298 assert !issue.closed?
2289 assert_nil issue.closed_on
2299 assert_nil issue.closed_on
2290 end
2300 end
2291
2301
2292 def test_closed_on_should_be_set_when_creating_a_closed_issue
2302 def test_closed_on_should_be_set_when_creating_a_closed_issue
2293 issue = Issue.generate!(:status_id => 5).reload
2303 issue = Issue.generate!(:status_id => 5).reload
2294 assert issue.closed?
2304 assert issue.closed?
2295 assert_not_nil issue.closed_on
2305 assert_not_nil issue.closed_on
2296 assert_equal issue.updated_on, issue.closed_on
2306 assert_equal issue.updated_on, issue.closed_on
2297 assert_equal issue.created_on, issue.closed_on
2307 assert_equal issue.created_on, issue.closed_on
2298 end
2308 end
2299
2309
2300 def test_closed_on_should_be_nil_when_updating_an_open_issue
2310 def test_closed_on_should_be_nil_when_updating_an_open_issue
2301 issue = Issue.find(1)
2311 issue = Issue.find(1)
2302 issue.subject = 'Not closed yet'
2312 issue.subject = 'Not closed yet'
2303 issue.save!
2313 issue.save!
2304 issue.reload
2314 issue.reload
2305 assert_nil issue.closed_on
2315 assert_nil issue.closed_on
2306 end
2316 end
2307
2317
2308 def test_closed_on_should_be_set_when_closing_an_open_issue
2318 def test_closed_on_should_be_set_when_closing_an_open_issue
2309 issue = Issue.find(1)
2319 issue = Issue.find(1)
2310 issue.subject = 'Now closed'
2320 issue.subject = 'Now closed'
2311 issue.status_id = 5
2321 issue.status_id = 5
2312 issue.save!
2322 issue.save!
2313 issue.reload
2323 issue.reload
2314 assert_not_nil issue.closed_on
2324 assert_not_nil issue.closed_on
2315 assert_equal issue.updated_on, issue.closed_on
2325 assert_equal issue.updated_on, issue.closed_on
2316 end
2326 end
2317
2327
2318 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2328 def test_closed_on_should_not_be_updated_when_updating_a_closed_issue
2319 issue = Issue.open(false).first
2329 issue = Issue.open(false).first
2320 was_closed_on = issue.closed_on
2330 was_closed_on = issue.closed_on
2321 assert_not_nil was_closed_on
2331 assert_not_nil was_closed_on
2322 issue.subject = 'Updating a closed issue'
2332 issue.subject = 'Updating a closed issue'
2323 issue.save!
2333 issue.save!
2324 issue.reload
2334 issue.reload
2325 assert_equal was_closed_on, issue.closed_on
2335 assert_equal was_closed_on, issue.closed_on
2326 end
2336 end
2327
2337
2328 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2338 def test_closed_on_should_be_preserved_when_reopening_a_closed_issue
2329 issue = Issue.open(false).first
2339 issue = Issue.open(false).first
2330 was_closed_on = issue.closed_on
2340 was_closed_on = issue.closed_on
2331 assert_not_nil was_closed_on
2341 assert_not_nil was_closed_on
2332 issue.subject = 'Reopening a closed issue'
2342 issue.subject = 'Reopening a closed issue'
2333 issue.status_id = 1
2343 issue.status_id = 1
2334 issue.save!
2344 issue.save!
2335 issue.reload
2345 issue.reload
2336 assert !issue.closed?
2346 assert !issue.closed?
2337 assert_equal was_closed_on, issue.closed_on
2347 assert_equal was_closed_on, issue.closed_on
2338 end
2348 end
2339
2349
2340 def test_status_was_should_return_nil_for_new_issue
2350 def test_status_was_should_return_nil_for_new_issue
2341 issue = Issue.new
2351 issue = Issue.new
2342 assert_nil issue.status_was
2352 assert_nil issue.status_was
2343 end
2353 end
2344
2354
2345 def test_status_was_should_return_status_before_change
2355 def test_status_was_should_return_status_before_change
2346 issue = Issue.find(1)
2356 issue = Issue.find(1)
2347 issue.status = IssueStatus.find(2)
2357 issue.status = IssueStatus.find(2)
2348 assert_equal IssueStatus.find(1), issue.status_was
2358 assert_equal IssueStatus.find(1), issue.status_was
2349 end
2359 end
2350
2360
2351 def test_status_was_should_be_reset_on_save
2361 def test_status_was_should_be_reset_on_save
2352 issue = Issue.find(1)
2362 issue = Issue.find(1)
2353 issue.status = IssueStatus.find(2)
2363 issue.status = IssueStatus.find(2)
2354 assert_equal IssueStatus.find(1), issue.status_was
2364 assert_equal IssueStatus.find(1), issue.status_was
2355 assert issue.save!
2365 assert issue.save!
2356 assert_equal IssueStatus.find(2), issue.status_was
2366 assert_equal IssueStatus.find(2), issue.status_was
2357 end
2367 end
2358 end
2368 end
General Comments 0
You need to be logged in to leave comments. Login now