##// END OF EJS Templates
Save an @Issue#save@....
Etienne Massip -
r8092:7d2298f39c60
parent child
Show More
@@ -1,87 +1,85
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 IssueMovesController < ApplicationController
18 class IssueMovesController < ApplicationController
19 menu_item :issues
19 menu_item :issues
20
20
21 default_search_scope :issues
21 default_search_scope :issues
22 before_filter :find_issues, :check_project_uniqueness
22 before_filter :find_issues, :check_project_uniqueness
23 before_filter :authorize
23 before_filter :authorize
24
24
25 def new
25 def new
26 prepare_for_issue_move
26 prepare_for_issue_move
27 render :layout => false if request.xhr?
27 render :layout => false if request.xhr?
28 end
28 end
29
29
30 def create
30 def create
31 prepare_for_issue_move
31 prepare_for_issue_move
32
32
33 if request.post?
33 if request.post?
34 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
34 new_tracker = params[:new_tracker_id].blank? ? nil : @target_project.trackers.find_by_id(params[:new_tracker_id])
35 unsaved_issue_ids = []
35 unsaved_issue_ids = []
36 moved_issues = []
36 moved_issues = []
37 @issues.each do |issue|
37 @issues.each do |issue|
38 issue.reload
38 issue.reload
39 issue.init_journal(User.current)
40 issue.current_journal.notes = @notes if @notes.present?
41 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
39 call_hook(:controller_issues_move_before_save, { :params => params, :issue => issue, :target_project => @target_project, :copy => !!@copy })
42 if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params)})
40 if r = issue.move_to_project(@target_project, new_tracker, {:copy => @copy, :attributes => extract_changed_attributes_for_move(params), :notes => @notes})
43 moved_issues << r
41 moved_issues << r
44 else
42 else
45 unsaved_issue_ids << issue.id
43 unsaved_issue_ids << issue.id
46 end
44 end
47 end
45 end
48 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
46 set_flash_from_bulk_issue_save(@issues, unsaved_issue_ids)
49
47
50 if params[:follow]
48 if params[:follow]
51 if @issues.size == 1 && moved_issues.size == 1
49 if @issues.size == 1 && moved_issues.size == 1
52 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
50 redirect_to :controller => 'issues', :action => 'show', :id => moved_issues.first
53 else
51 else
54 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
52 redirect_to :controller => 'issues', :action => 'index', :project_id => (@target_project || @project)
55 end
53 end
56 else
54 else
57 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
55 redirect_to :controller => 'issues', :action => 'index', :project_id => @project
58 end
56 end
59 return
57 return
60 end
58 end
61 end
59 end
62
60
63 private
61 private
64
62
65 def prepare_for_issue_move
63 def prepare_for_issue_move
66 @issues.sort!
64 @issues.sort!
67 @copy = params[:copy_options] && params[:copy_options][:copy]
65 @copy = params[:copy_options] && params[:copy_options][:copy]
68 @allowed_projects = Issue.allowed_target_projects_on_move
66 @allowed_projects = Issue.allowed_target_projects_on_move
69 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
67 @target_project = @allowed_projects.detect {|p| p.id.to_s == params[:new_project_id]} if params[:new_project_id]
70 @target_project ||= @project
68 @target_project ||= @project
71 @trackers = @target_project.trackers
69 @trackers = @target_project.trackers
72 @available_statuses = Workflow.available_statuses(@project)
70 @available_statuses = Workflow.available_statuses(@project)
73 @notes = params[:notes]
71 @notes = params[:notes]
74 @notes ||= ''
72 @notes ||= ''
75 end
73 end
76
74
77 def extract_changed_attributes_for_move(params)
75 def extract_changed_attributes_for_move(params)
78 changed_attributes = {}
76 changed_attributes = {}
79 [:assigned_to_id, :status_id, :start_date, :due_date, :priority_id].each do |valid_attribute|
77 [:assigned_to_id, :status_id, :start_date, :due_date, :priority_id].each do |valid_attribute|
80 unless params[valid_attribute].blank?
78 unless params[valid_attribute].blank?
81 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
79 changed_attributes[valid_attribute] = (params[valid_attribute] == 'none' ? nil : params[valid_attribute])
82 end
80 end
83 end
81 end
84 changed_attributes
82 changed_attributes
85 end
83 end
86
84
87 end
85 end
@@ -1,982 +1,986
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue
61 validate :validate_issue
62
62
63 named_scope :visible, lambda {|*args| { :include => :project,
63 named_scope :visible, lambda {|*args| { :include => :project,
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
64 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65
65
66 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
66 named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
67
67
68 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
68 named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
69 named_scope :with_limit, lambda { |limit| { :limit => limit} }
69 named_scope :with_limit, lambda { |limit| { :limit => limit} }
70 named_scope :on_active_project, :include => [:status, :project, :tracker],
70 named_scope :on_active_project, :include => [:status, :project, :tracker],
71 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
71 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
72
72
73 named_scope :without_version, lambda {
73 named_scope :without_version, lambda {
74 {
74 {
75 :conditions => { :fixed_version_id => nil}
75 :conditions => { :fixed_version_id => nil}
76 }
76 }
77 }
77 }
78
78
79 named_scope :with_query, lambda {|query|
79 named_scope :with_query, lambda {|query|
80 {
80 {
81 :conditions => Query.merge_conditions(query.statement)
81 :conditions => Query.merge_conditions(query.statement)
82 }
82 }
83 }
83 }
84
84
85 before_create :default_assign
85 before_create :default_assign
86 before_save :close_duplicates, :update_done_ratio_from_issue_status
86 before_save :close_duplicates, :update_done_ratio_from_issue_status
87 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
87 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
88 after_destroy :update_parent_attributes
88 after_destroy :update_parent_attributes
89
89
90 # Returns a SQL conditions string used to find all issues visible by the specified user
90 # Returns a SQL conditions string used to find all issues visible by the specified user
91 def self.visible_condition(user, options={})
91 def self.visible_condition(user, options={})
92 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
92 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
93 case role.issues_visibility
93 case role.issues_visibility
94 when 'all'
94 when 'all'
95 nil
95 nil
96 when 'default'
96 when 'default'
97 user_ids = [user.id] + user.groups.map(&:id)
97 user_ids = [user.id] + user.groups.map(&:id)
98 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
98 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
99 when 'own'
99 when 'own'
100 user_ids = [user.id] + user.groups.map(&:id)
100 user_ids = [user.id] + user.groups.map(&:id)
101 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
101 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
102 else
102 else
103 '1=0'
103 '1=0'
104 end
104 end
105 end
105 end
106 end
106 end
107
107
108 # Returns true if usr or current user is allowed to view the issue
108 # Returns true if usr or current user is allowed to view the issue
109 def visible?(usr=nil)
109 def visible?(usr=nil)
110 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
110 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
111 case role.issues_visibility
111 case role.issues_visibility
112 when 'all'
112 when 'all'
113 true
113 true
114 when 'default'
114 when 'default'
115 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
115 !self.is_private? || self.author == user || user.is_or_belongs_to?(assigned_to)
116 when 'own'
116 when 'own'
117 self.author == user || user.is_or_belongs_to?(assigned_to)
117 self.author == user || user.is_or_belongs_to?(assigned_to)
118 else
118 else
119 false
119 false
120 end
120 end
121 end
121 end
122 end
122 end
123
123
124 def after_initialize
124 def after_initialize
125 if new_record?
125 if new_record?
126 # set default values for new records only
126 # set default values for new records only
127 self.status ||= IssueStatus.default
127 self.status ||= IssueStatus.default
128 self.priority ||= IssuePriority.default
128 self.priority ||= IssuePriority.default
129 end
129 end
130 end
130 end
131
131
132 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
132 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
133 def available_custom_fields
133 def available_custom_fields
134 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
134 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
135 end
135 end
136
136
137 def copy_from(arg)
137 def copy_from(arg)
138 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
138 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
139 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
139 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
140 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
140 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
141 self.status = issue.status
141 self.status = issue.status
142 self
142 self
143 end
143 end
144
144
145 # Moves/copies an issue to a new project and tracker
145 # Moves/copies an issue to a new project and tracker
146 # Returns the moved/copied issue on success, false on failure
146 # Returns the moved/copied issue on success, false on failure
147 def move_to_project(*args)
147 def move_to_project(*args)
148 ret = Issue.transaction do
148 ret = Issue.transaction do
149 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
149 move_to_project_without_transaction(*args) || raise(ActiveRecord::Rollback)
150 end || false
150 end || false
151 end
151 end
152
152
153 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
153 def move_to_project_without_transaction(new_project, new_tracker = nil, options = {})
154 options ||= {}
154 options ||= {}
155 issue = options[:copy] ? self.class.new.copy_from(self) : self
155
156 if options[:copy]
157 issue = self.class.new.copy_from(self)
158 else
159 issue = self
160 issue.init_journal(User.current, options[:notes])
161 end
156
162
157 if new_project && issue.project_id != new_project.id
163 if new_project && issue.project_id != new_project.id
158 # delete issue relations
164 # delete issue relations
159 unless Setting.cross_project_issue_relations?
165 unless Setting.cross_project_issue_relations?
160 issue.relations_from.clear
166 issue.relations_from.clear
161 issue.relations_to.clear
167 issue.relations_to.clear
162 end
168 end
163 # issue is moved to another project
169 # issue is moved to another project
164 # reassign to the category with same name if any
170 # reassign to the category with same name if any
165 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
171 new_category = issue.category.nil? ? nil : new_project.issue_categories.find_by_name(issue.category.name)
166 issue.category = new_category
172 issue.category = new_category
167 # Keep the fixed_version if it's still valid in the new_project
173 # Keep the fixed_version if it's still valid in the new_project
168 unless new_project.shared_versions.include?(issue.fixed_version)
174 unless new_project.shared_versions.include?(issue.fixed_version)
169 issue.fixed_version = nil
175 issue.fixed_version = nil
170 end
176 end
171 issue.project = new_project
177 issue.project = new_project
172 if issue.parent && issue.parent.project_id != issue.project_id
178 if issue.parent && issue.parent.project_id != issue.project_id
173 issue.parent_issue_id = nil
179 issue.parent_issue_id = nil
174 end
180 end
175 end
181 end
176 if new_tracker
182 if new_tracker
177 issue.tracker = new_tracker
183 issue.tracker = new_tracker
178 issue.reset_custom_values!
184 issue.reset_custom_values!
179 end
185 end
180 if options[:copy]
186 if options[:copy]
181 issue.author = User.current
187 issue.author = User.current
182 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
188 issue.custom_field_values = self.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
183 issue.status = if options[:attributes] && options[:attributes][:status_id]
189 issue.status = if options[:attributes] && options[:attributes][:status_id]
184 IssueStatus.find_by_id(options[:attributes][:status_id])
190 IssueStatus.find_by_id(options[:attributes][:status_id])
185 else
191 else
186 self.status
192 self.status
187 end
193 end
188 end
194 end
189 # Allow bulk setting of attributes on the issue
195 # Allow bulk setting of attributes on the issue
190 if options[:attributes]
196 if options[:attributes]
191 issue.attributes = options[:attributes]
197 issue.attributes = options[:attributes]
192 end
198 end
199 if options[:copy] && options[:notes].present?
200 issue.init_journal(User.current, options[:notes])
201 issue.current_journal.notify = false
202 end
193 if issue.save
203 if issue.save
194 if options[:copy]
204 unless options[:copy]
195 if current_journal && current_journal.notes.present?
196 issue.init_journal(current_journal.user, current_journal.notes)
197 issue.current_journal.notify = false
198 issue.save
199 end
200 else
201 # Manually update project_id on related time entries
205 # Manually update project_id on related time entries
202 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
206 TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
203
207
204 issue.children.each do |child|
208 issue.children.each do |child|
205 unless child.move_to_project_without_transaction(new_project)
209 unless child.move_to_project_without_transaction(new_project)
206 # Move failed and transaction was rollback'd
210 # Move failed and transaction was rollback'd
207 return false
211 return false
208 end
212 end
209 end
213 end
210 end
214 end
211 else
215 else
212 return false
216 return false
213 end
217 end
214 issue
218 issue
215 end
219 end
216
220
217 def status_id=(sid)
221 def status_id=(sid)
218 self.status = nil
222 self.status = nil
219 write_attribute(:status_id, sid)
223 write_attribute(:status_id, sid)
220 end
224 end
221
225
222 def priority_id=(pid)
226 def priority_id=(pid)
223 self.priority = nil
227 self.priority = nil
224 write_attribute(:priority_id, pid)
228 write_attribute(:priority_id, pid)
225 end
229 end
226
230
227 def tracker_id=(tid)
231 def tracker_id=(tid)
228 self.tracker = nil
232 self.tracker = nil
229 result = write_attribute(:tracker_id, tid)
233 result = write_attribute(:tracker_id, tid)
230 @custom_field_values = nil
234 @custom_field_values = nil
231 result
235 result
232 end
236 end
233
237
234 def description=(arg)
238 def description=(arg)
235 if arg.is_a?(String)
239 if arg.is_a?(String)
236 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
240 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
237 end
241 end
238 write_attribute(:description, arg)
242 write_attribute(:description, arg)
239 end
243 end
240
244
241 # Overrides attributes= so that project and tracker get assigned first
245 # Overrides attributes= so that project and tracker get assigned first
242 def attributes_with_project_and_tracker_first=(new_attributes, *args)
246 def attributes_with_project_and_tracker_first=(new_attributes, *args)
243 return if new_attributes.nil?
247 return if new_attributes.nil?
244 attrs = new_attributes.dup
248 attrs = new_attributes.dup
245 attrs.stringify_keys!
249 attrs.stringify_keys!
246
250
247 %w(project project_id tracker tracker_id).each do |attr|
251 %w(project project_id tracker tracker_id).each do |attr|
248 if attrs.has_key?(attr)
252 if attrs.has_key?(attr)
249 send "#{attr}=", attrs.delete(attr)
253 send "#{attr}=", attrs.delete(attr)
250 end
254 end
251 end
255 end
252 send :attributes_without_project_and_tracker_first=, attrs, *args
256 send :attributes_without_project_and_tracker_first=, attrs, *args
253 end
257 end
254 # Do not redefine alias chain on reload (see #4838)
258 # Do not redefine alias chain on reload (see #4838)
255 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
259 alias_method_chain(:attributes=, :project_and_tracker_first) unless method_defined?(:attributes_without_project_and_tracker_first=)
256
260
257 def estimated_hours=(h)
261 def estimated_hours=(h)
258 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
262 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
259 end
263 end
260
264
261 safe_attributes 'tracker_id',
265 safe_attributes 'tracker_id',
262 'status_id',
266 'status_id',
263 'category_id',
267 'category_id',
264 'assigned_to_id',
268 'assigned_to_id',
265 'priority_id',
269 'priority_id',
266 'fixed_version_id',
270 'fixed_version_id',
267 'subject',
271 'subject',
268 'description',
272 'description',
269 'start_date',
273 'start_date',
270 'due_date',
274 'due_date',
271 'done_ratio',
275 'done_ratio',
272 'estimated_hours',
276 'estimated_hours',
273 'custom_field_values',
277 'custom_field_values',
274 'custom_fields',
278 'custom_fields',
275 'lock_version',
279 'lock_version',
276 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
280 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
277
281
278 safe_attributes 'status_id',
282 safe_attributes 'status_id',
279 'assigned_to_id',
283 'assigned_to_id',
280 'fixed_version_id',
284 'fixed_version_id',
281 'done_ratio',
285 'done_ratio',
282 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
286 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
283
287
284 safe_attributes 'watcher_user_ids',
288 safe_attributes 'watcher_user_ids',
285 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
289 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
286
290
287 safe_attributes 'is_private',
291 safe_attributes 'is_private',
288 :if => lambda {|issue, user|
292 :if => lambda {|issue, user|
289 user.allowed_to?(:set_issues_private, issue.project) ||
293 user.allowed_to?(:set_issues_private, issue.project) ||
290 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
294 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
291 }
295 }
292
296
293 safe_attributes 'parent_issue_id',
297 safe_attributes 'parent_issue_id',
294 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
298 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
295 user.allowed_to?(:manage_subtasks, issue.project)}
299 user.allowed_to?(:manage_subtasks, issue.project)}
296
300
297 # Safely sets attributes
301 # Safely sets attributes
298 # Should be called from controllers instead of #attributes=
302 # Should be called from controllers instead of #attributes=
299 # attr_accessible is too rough because we still want things like
303 # attr_accessible is too rough because we still want things like
300 # Issue.new(:project => foo) to work
304 # Issue.new(:project => foo) to work
301 # TODO: move workflow/permission checks from controllers to here
305 # TODO: move workflow/permission checks from controllers to here
302 def safe_attributes=(attrs, user=User.current)
306 def safe_attributes=(attrs, user=User.current)
303 return unless attrs.is_a?(Hash)
307 return unless attrs.is_a?(Hash)
304
308
305 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
309 # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
306 attrs = delete_unsafe_attributes(attrs, user)
310 attrs = delete_unsafe_attributes(attrs, user)
307 return if attrs.empty?
311 return if attrs.empty?
308
312
309 # Tracker must be set before since new_statuses_allowed_to depends on it.
313 # Tracker must be set before since new_statuses_allowed_to depends on it.
310 if t = attrs.delete('tracker_id')
314 if t = attrs.delete('tracker_id')
311 self.tracker_id = t
315 self.tracker_id = t
312 end
316 end
313
317
314 if attrs['status_id']
318 if attrs['status_id']
315 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
319 unless new_statuses_allowed_to(user).collect(&:id).include?(attrs['status_id'].to_i)
316 attrs.delete('status_id')
320 attrs.delete('status_id')
317 end
321 end
318 end
322 end
319
323
320 unless leaf?
324 unless leaf?
321 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
325 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
322 end
326 end
323
327
324 if attrs['parent_issue_id'].present?
328 if attrs['parent_issue_id'].present?
325 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
329 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
326 end
330 end
327
331
328 # mass-assignment security bypass
332 # mass-assignment security bypass
329 self.send :attributes=, attrs, false
333 self.send :attributes=, attrs, false
330 end
334 end
331
335
332 def done_ratio
336 def done_ratio
333 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
337 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
334 status.default_done_ratio
338 status.default_done_ratio
335 else
339 else
336 read_attribute(:done_ratio)
340 read_attribute(:done_ratio)
337 end
341 end
338 end
342 end
339
343
340 def self.use_status_for_done_ratio?
344 def self.use_status_for_done_ratio?
341 Setting.issue_done_ratio == 'issue_status'
345 Setting.issue_done_ratio == 'issue_status'
342 end
346 end
343
347
344 def self.use_field_for_done_ratio?
348 def self.use_field_for_done_ratio?
345 Setting.issue_done_ratio == 'issue_field'
349 Setting.issue_done_ratio == 'issue_field'
346 end
350 end
347
351
348 def validate_issue
352 def validate_issue
349 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
353 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
350 errors.add :due_date, :not_a_date
354 errors.add :due_date, :not_a_date
351 end
355 end
352
356
353 if self.due_date and self.start_date and self.due_date < self.start_date
357 if self.due_date and self.start_date and self.due_date < self.start_date
354 errors.add :due_date, :greater_than_start_date
358 errors.add :due_date, :greater_than_start_date
355 end
359 end
356
360
357 if start_date && soonest_start && start_date < soonest_start
361 if start_date && soonest_start && start_date < soonest_start
358 errors.add :start_date, :invalid
362 errors.add :start_date, :invalid
359 end
363 end
360
364
361 if fixed_version
365 if fixed_version
362 if !assignable_versions.include?(fixed_version)
366 if !assignable_versions.include?(fixed_version)
363 errors.add :fixed_version_id, :inclusion
367 errors.add :fixed_version_id, :inclusion
364 elsif reopened? && fixed_version.closed?
368 elsif reopened? && fixed_version.closed?
365 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
369 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
366 end
370 end
367 end
371 end
368
372
369 # Checks that the issue can not be added/moved to a disabled tracker
373 # Checks that the issue can not be added/moved to a disabled tracker
370 if project && (tracker_id_changed? || project_id_changed?)
374 if project && (tracker_id_changed? || project_id_changed?)
371 unless project.trackers.include?(tracker)
375 unless project.trackers.include?(tracker)
372 errors.add :tracker_id, :inclusion
376 errors.add :tracker_id, :inclusion
373 end
377 end
374 end
378 end
375
379
376 # Checks parent issue assignment
380 # Checks parent issue assignment
377 if @parent_issue
381 if @parent_issue
378 if @parent_issue.project_id != project_id
382 if @parent_issue.project_id != project_id
379 errors.add :parent_issue_id, :not_same_project
383 errors.add :parent_issue_id, :not_same_project
380 elsif !new_record?
384 elsif !new_record?
381 # moving an existing issue
385 # moving an existing issue
382 if @parent_issue.root_id != root_id
386 if @parent_issue.root_id != root_id
383 # we can always move to another tree
387 # we can always move to another tree
384 elsif move_possible?(@parent_issue)
388 elsif move_possible?(@parent_issue)
385 # move accepted inside tree
389 # move accepted inside tree
386 else
390 else
387 errors.add :parent_issue_id, :not_a_valid_parent
391 errors.add :parent_issue_id, :not_a_valid_parent
388 end
392 end
389 end
393 end
390 end
394 end
391 end
395 end
392
396
393 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
397 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
394 # even if the user turns off the setting later
398 # even if the user turns off the setting later
395 def update_done_ratio_from_issue_status
399 def update_done_ratio_from_issue_status
396 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
400 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
397 self.done_ratio = status.default_done_ratio
401 self.done_ratio = status.default_done_ratio
398 end
402 end
399 end
403 end
400
404
401 def init_journal(user, notes = "")
405 def init_journal(user, notes = "")
402 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
406 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
403 @issue_before_change = self.clone
407 @issue_before_change = self.clone
404 @issue_before_change.status = self.status
408 @issue_before_change.status = self.status
405 @custom_values_before_change = {}
409 @custom_values_before_change = {}
406 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
410 self.custom_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
407 # Make sure updated_on is updated when adding a note.
411 # Make sure updated_on is updated when adding a note.
408 updated_on_will_change!
412 updated_on_will_change!
409 @current_journal
413 @current_journal
410 end
414 end
411
415
412 # Return true if the issue is closed, otherwise false
416 # Return true if the issue is closed, otherwise false
413 def closed?
417 def closed?
414 self.status.is_closed?
418 self.status.is_closed?
415 end
419 end
416
420
417 # Return true if the issue is being reopened
421 # Return true if the issue is being reopened
418 def reopened?
422 def reopened?
419 if !new_record? && status_id_changed?
423 if !new_record? && status_id_changed?
420 status_was = IssueStatus.find_by_id(status_id_was)
424 status_was = IssueStatus.find_by_id(status_id_was)
421 status_new = IssueStatus.find_by_id(status_id)
425 status_new = IssueStatus.find_by_id(status_id)
422 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
426 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
423 return true
427 return true
424 end
428 end
425 end
429 end
426 false
430 false
427 end
431 end
428
432
429 # Return true if the issue is being closed
433 # Return true if the issue is being closed
430 def closing?
434 def closing?
431 if !new_record? && status_id_changed?
435 if !new_record? && status_id_changed?
432 status_was = IssueStatus.find_by_id(status_id_was)
436 status_was = IssueStatus.find_by_id(status_id_was)
433 status_new = IssueStatus.find_by_id(status_id)
437 status_new = IssueStatus.find_by_id(status_id)
434 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
438 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
435 return true
439 return true
436 end
440 end
437 end
441 end
438 false
442 false
439 end
443 end
440
444
441 # Returns true if the issue is overdue
445 # Returns true if the issue is overdue
442 def overdue?
446 def overdue?
443 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
447 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
444 end
448 end
445
449
446 # Is the amount of work done less than it should for the due date
450 # Is the amount of work done less than it should for the due date
447 def behind_schedule?
451 def behind_schedule?
448 return false if start_date.nil? || due_date.nil?
452 return false if start_date.nil? || due_date.nil?
449 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
453 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
450 return done_date <= Date.today
454 return done_date <= Date.today
451 end
455 end
452
456
453 # Does this issue have children?
457 # Does this issue have children?
454 def children?
458 def children?
455 !leaf?
459 !leaf?
456 end
460 end
457
461
458 # Users the issue can be assigned to
462 # Users the issue can be assigned to
459 def assignable_users
463 def assignable_users
460 users = project.assignable_users
464 users = project.assignable_users
461 users << author if author
465 users << author if author
462 users << assigned_to if assigned_to
466 users << assigned_to if assigned_to
463 users.uniq.sort
467 users.uniq.sort
464 end
468 end
465
469
466 # Versions that the issue can be assigned to
470 # Versions that the issue can be assigned to
467 def assignable_versions
471 def assignable_versions
468 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
472 @assignable_versions ||= (project.shared_versions.open + [Version.find_by_id(fixed_version_id_was)]).compact.uniq.sort
469 end
473 end
470
474
471 # Returns true if this issue is blocked by another issue that is still open
475 # Returns true if this issue is blocked by another issue that is still open
472 def blocked?
476 def blocked?
473 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
477 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
474 end
478 end
475
479
476 # Returns an array of status that user is able to apply
480 # Returns an array of status that user is able to apply
477 def new_statuses_allowed_to(user, include_default=false)
481 def new_statuses_allowed_to(user, include_default=false)
478 statuses = status.find_new_statuses_allowed_to(
482 statuses = status.find_new_statuses_allowed_to(
479 user.roles_for_project(project),
483 user.roles_for_project(project),
480 tracker,
484 tracker,
481 author == user,
485 author == user,
482 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
486 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
483 )
487 )
484 statuses << status unless statuses.empty?
488 statuses << status unless statuses.empty?
485 statuses << IssueStatus.default if include_default
489 statuses << IssueStatus.default if include_default
486 statuses = statuses.uniq.sort
490 statuses = statuses.uniq.sort
487 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
491 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
488 end
492 end
489
493
490 # Returns the mail adresses of users that should be notified
494 # Returns the mail adresses of users that should be notified
491 def recipients
495 def recipients
492 notified = project.notified_users
496 notified = project.notified_users
493 # Author and assignee are always notified unless they have been
497 # Author and assignee are always notified unless they have been
494 # locked or don't want to be notified
498 # locked or don't want to be notified
495 notified << author if author && author.active? && author.notify_about?(self)
499 notified << author if author && author.active? && author.notify_about?(self)
496 if assigned_to
500 if assigned_to
497 if assigned_to.is_a?(Group)
501 if assigned_to.is_a?(Group)
498 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
502 notified += assigned_to.users.select {|u| u.active? && u.notify_about?(self)}
499 else
503 else
500 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
504 notified << assigned_to if assigned_to.active? && assigned_to.notify_about?(self)
501 end
505 end
502 end
506 end
503 notified.uniq!
507 notified.uniq!
504 # Remove users that can not view the issue
508 # Remove users that can not view the issue
505 notified.reject! {|user| !visible?(user)}
509 notified.reject! {|user| !visible?(user)}
506 notified.collect(&:mail)
510 notified.collect(&:mail)
507 end
511 end
508
512
509 # Returns the number of hours spent on this issue
513 # Returns the number of hours spent on this issue
510 def spent_hours
514 def spent_hours
511 @spent_hours ||= time_entries.sum(:hours) || 0
515 @spent_hours ||= time_entries.sum(:hours) || 0
512 end
516 end
513
517
514 # Returns the total number of hours spent on this issue and its descendants
518 # Returns the total number of hours spent on this issue and its descendants
515 #
519 #
516 # Example:
520 # Example:
517 # spent_hours => 0.0
521 # spent_hours => 0.0
518 # spent_hours => 50.2
522 # spent_hours => 50.2
519 def total_spent_hours
523 def total_spent_hours
520 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
524 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours", :include => :time_entries).to_f || 0.0
521 end
525 end
522
526
523 def relations
527 def relations
524 @relations ||= (relations_from + relations_to).sort
528 @relations ||= (relations_from + relations_to).sort
525 end
529 end
526
530
527 # Preloads relations for a collection of issues
531 # Preloads relations for a collection of issues
528 def self.load_relations(issues)
532 def self.load_relations(issues)
529 if issues.any?
533 if issues.any?
530 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
534 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
531 issues.each do |issue|
535 issues.each do |issue|
532 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
536 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
533 end
537 end
534 end
538 end
535 end
539 end
536
540
537 # Preloads visible spent time for a collection of issues
541 # Preloads visible spent time for a collection of issues
538 def self.load_visible_spent_hours(issues, user=User.current)
542 def self.load_visible_spent_hours(issues, user=User.current)
539 if issues.any?
543 if issues.any?
540 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
544 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
541 issues.each do |issue|
545 issues.each do |issue|
542 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
546 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
543 end
547 end
544 end
548 end
545 end
549 end
546
550
547 # Finds an issue relation given its id.
551 # Finds an issue relation given its id.
548 def find_relation(relation_id)
552 def find_relation(relation_id)
549 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
553 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
550 end
554 end
551
555
552 def all_dependent_issues(except=[])
556 def all_dependent_issues(except=[])
553 except << self
557 except << self
554 dependencies = []
558 dependencies = []
555 relations_from.each do |relation|
559 relations_from.each do |relation|
556 if relation.issue_to && !except.include?(relation.issue_to)
560 if relation.issue_to && !except.include?(relation.issue_to)
557 dependencies << relation.issue_to
561 dependencies << relation.issue_to
558 dependencies += relation.issue_to.all_dependent_issues(except)
562 dependencies += relation.issue_to.all_dependent_issues(except)
559 end
563 end
560 end
564 end
561 dependencies
565 dependencies
562 end
566 end
563
567
564 # Returns an array of issues that duplicate this one
568 # Returns an array of issues that duplicate this one
565 def duplicates
569 def duplicates
566 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
570 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
567 end
571 end
568
572
569 # Returns the due date or the target due date if any
573 # Returns the due date or the target due date if any
570 # Used on gantt chart
574 # Used on gantt chart
571 def due_before
575 def due_before
572 due_date || (fixed_version ? fixed_version.effective_date : nil)
576 due_date || (fixed_version ? fixed_version.effective_date : nil)
573 end
577 end
574
578
575 # Returns the time scheduled for this issue.
579 # Returns the time scheduled for this issue.
576 #
580 #
577 # Example:
581 # Example:
578 # Start Date: 2/26/09, End Date: 3/04/09
582 # Start Date: 2/26/09, End Date: 3/04/09
579 # duration => 6
583 # duration => 6
580 def duration
584 def duration
581 (start_date && due_date) ? due_date - start_date : 0
585 (start_date && due_date) ? due_date - start_date : 0
582 end
586 end
583
587
584 def soonest_start
588 def soonest_start
585 @soonest_start ||= (
589 @soonest_start ||= (
586 relations_to.collect{|relation| relation.successor_soonest_start} +
590 relations_to.collect{|relation| relation.successor_soonest_start} +
587 ancestors.collect(&:soonest_start)
591 ancestors.collect(&:soonest_start)
588 ).compact.max
592 ).compact.max
589 end
593 end
590
594
591 def reschedule_after(date)
595 def reschedule_after(date)
592 return if date.nil?
596 return if date.nil?
593 if leaf?
597 if leaf?
594 if start_date.nil? || start_date < date
598 if start_date.nil? || start_date < date
595 self.start_date, self.due_date = date, date + duration
599 self.start_date, self.due_date = date, date + duration
596 save
600 save
597 end
601 end
598 else
602 else
599 leaves.each do |leaf|
603 leaves.each do |leaf|
600 leaf.reschedule_after(date)
604 leaf.reschedule_after(date)
601 end
605 end
602 end
606 end
603 end
607 end
604
608
605 def <=>(issue)
609 def <=>(issue)
606 if issue.nil?
610 if issue.nil?
607 -1
611 -1
608 elsif root_id != issue.root_id
612 elsif root_id != issue.root_id
609 (root_id || 0) <=> (issue.root_id || 0)
613 (root_id || 0) <=> (issue.root_id || 0)
610 else
614 else
611 (lft || 0) <=> (issue.lft || 0)
615 (lft || 0) <=> (issue.lft || 0)
612 end
616 end
613 end
617 end
614
618
615 def to_s
619 def to_s
616 "#{tracker} ##{id}: #{subject}"
620 "#{tracker} ##{id}: #{subject}"
617 end
621 end
618
622
619 # Returns a string of css classes that apply to the issue
623 # Returns a string of css classes that apply to the issue
620 def css_classes
624 def css_classes
621 s = "issue status-#{status.position} priority-#{priority.position}"
625 s = "issue status-#{status.position} priority-#{priority.position}"
622 s << ' closed' if closed?
626 s << ' closed' if closed?
623 s << ' overdue' if overdue?
627 s << ' overdue' if overdue?
624 s << ' child' if child?
628 s << ' child' if child?
625 s << ' parent' unless leaf?
629 s << ' parent' unless leaf?
626 s << ' private' if is_private?
630 s << ' private' if is_private?
627 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
631 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
628 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
632 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
629 s
633 s
630 end
634 end
631
635
632 # Saves an issue, time_entry, attachments, and a journal from the parameters
636 # Saves an issue, time_entry, attachments, and a journal from the parameters
633 # Returns false if save fails
637 # Returns false if save fails
634 def save_issue_with_child_records(params, existing_time_entry=nil)
638 def save_issue_with_child_records(params, existing_time_entry=nil)
635 Issue.transaction do
639 Issue.transaction do
636 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
640 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
637 @time_entry = existing_time_entry || TimeEntry.new
641 @time_entry = existing_time_entry || TimeEntry.new
638 @time_entry.project = project
642 @time_entry.project = project
639 @time_entry.issue = self
643 @time_entry.issue = self
640 @time_entry.user = User.current
644 @time_entry.user = User.current
641 @time_entry.spent_on = User.current.today
645 @time_entry.spent_on = User.current.today
642 @time_entry.attributes = params[:time_entry]
646 @time_entry.attributes = params[:time_entry]
643 self.time_entries << @time_entry
647 self.time_entries << @time_entry
644 end
648 end
645
649
646 if valid?
650 if valid?
647 attachments = Attachment.attach_files(self, params[:attachments])
651 attachments = Attachment.attach_files(self, params[:attachments])
648 # TODO: Rename hook
652 # TODO: Rename hook
649 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
653 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
650 begin
654 begin
651 if save
655 if save
652 # TODO: Rename hook
656 # TODO: Rename hook
653 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
657 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
654 else
658 else
655 raise ActiveRecord::Rollback
659 raise ActiveRecord::Rollback
656 end
660 end
657 rescue ActiveRecord::StaleObjectError
661 rescue ActiveRecord::StaleObjectError
658 attachments[:files].each(&:destroy)
662 attachments[:files].each(&:destroy)
659 errors.add :base, l(:notice_locking_conflict)
663 errors.add :base, l(:notice_locking_conflict)
660 raise ActiveRecord::Rollback
664 raise ActiveRecord::Rollback
661 end
665 end
662 end
666 end
663 end
667 end
664 end
668 end
665
669
666 # Unassigns issues from +version+ if it's no longer shared with issue's project
670 # Unassigns issues from +version+ if it's no longer shared with issue's project
667 def self.update_versions_from_sharing_change(version)
671 def self.update_versions_from_sharing_change(version)
668 # Update issues assigned to the version
672 # Update issues assigned to the version
669 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
673 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
670 end
674 end
671
675
672 # Unassigns issues from versions that are no longer shared
676 # Unassigns issues from versions that are no longer shared
673 # after +project+ was moved
677 # after +project+ was moved
674 def self.update_versions_from_hierarchy_change(project)
678 def self.update_versions_from_hierarchy_change(project)
675 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
679 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
676 # Update issues of the moved projects and issues assigned to a version of a moved project
680 # Update issues of the moved projects and issues assigned to a version of a moved project
677 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
681 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
678 end
682 end
679
683
680 def parent_issue_id=(arg)
684 def parent_issue_id=(arg)
681 parent_issue_id = arg.blank? ? nil : arg.to_i
685 parent_issue_id = arg.blank? ? nil : arg.to_i
682 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
686 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
683 @parent_issue.id
687 @parent_issue.id
684 else
688 else
685 @parent_issue = nil
689 @parent_issue = nil
686 nil
690 nil
687 end
691 end
688 end
692 end
689
693
690 def parent_issue_id
694 def parent_issue_id
691 if instance_variable_defined? :@parent_issue
695 if instance_variable_defined? :@parent_issue
692 @parent_issue.nil? ? nil : @parent_issue.id
696 @parent_issue.nil? ? nil : @parent_issue.id
693 else
697 else
694 parent_id
698 parent_id
695 end
699 end
696 end
700 end
697
701
698 # Extracted from the ReportsController.
702 # Extracted from the ReportsController.
699 def self.by_tracker(project)
703 def self.by_tracker(project)
700 count_and_group_by(:project => project,
704 count_and_group_by(:project => project,
701 :field => 'tracker_id',
705 :field => 'tracker_id',
702 :joins => Tracker.table_name)
706 :joins => Tracker.table_name)
703 end
707 end
704
708
705 def self.by_version(project)
709 def self.by_version(project)
706 count_and_group_by(:project => project,
710 count_and_group_by(:project => project,
707 :field => 'fixed_version_id',
711 :field => 'fixed_version_id',
708 :joins => Version.table_name)
712 :joins => Version.table_name)
709 end
713 end
710
714
711 def self.by_priority(project)
715 def self.by_priority(project)
712 count_and_group_by(:project => project,
716 count_and_group_by(:project => project,
713 :field => 'priority_id',
717 :field => 'priority_id',
714 :joins => IssuePriority.table_name)
718 :joins => IssuePriority.table_name)
715 end
719 end
716
720
717 def self.by_category(project)
721 def self.by_category(project)
718 count_and_group_by(:project => project,
722 count_and_group_by(:project => project,
719 :field => 'category_id',
723 :field => 'category_id',
720 :joins => IssueCategory.table_name)
724 :joins => IssueCategory.table_name)
721 end
725 end
722
726
723 def self.by_assigned_to(project)
727 def self.by_assigned_to(project)
724 count_and_group_by(:project => project,
728 count_and_group_by(:project => project,
725 :field => 'assigned_to_id',
729 :field => 'assigned_to_id',
726 :joins => User.table_name)
730 :joins => User.table_name)
727 end
731 end
728
732
729 def self.by_author(project)
733 def self.by_author(project)
730 count_and_group_by(:project => project,
734 count_and_group_by(:project => project,
731 :field => 'author_id',
735 :field => 'author_id',
732 :joins => User.table_name)
736 :joins => User.table_name)
733 end
737 end
734
738
735 def self.by_subproject(project)
739 def self.by_subproject(project)
736 ActiveRecord::Base.connection.select_all("select s.id as status_id,
740 ActiveRecord::Base.connection.select_all("select s.id as status_id,
737 s.is_closed as closed,
741 s.is_closed as closed,
738 #{Issue.table_name}.project_id as project_id,
742 #{Issue.table_name}.project_id as project_id,
739 count(#{Issue.table_name}.id) as total
743 count(#{Issue.table_name}.id) as total
740 from
744 from
741 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
745 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
742 where
746 where
743 #{Issue.table_name}.status_id=s.id
747 #{Issue.table_name}.status_id=s.id
744 and #{Issue.table_name}.project_id = #{Project.table_name}.id
748 and #{Issue.table_name}.project_id = #{Project.table_name}.id
745 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
749 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
746 and #{Issue.table_name}.project_id <> #{project.id}
750 and #{Issue.table_name}.project_id <> #{project.id}
747 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
751 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
748 end
752 end
749 # End ReportsController extraction
753 # End ReportsController extraction
750
754
751 # Returns an array of projects that current user can move issues to
755 # Returns an array of projects that current user can move issues to
752 def self.allowed_target_projects_on_move
756 def self.allowed_target_projects_on_move
753 projects = []
757 projects = []
754 if User.current.admin?
758 if User.current.admin?
755 # admin is allowed to move issues to any active (visible) project
759 # admin is allowed to move issues to any active (visible) project
756 projects = Project.visible.all
760 projects = Project.visible.all
757 elsif User.current.logged?
761 elsif User.current.logged?
758 if Role.non_member.allowed_to?(:move_issues)
762 if Role.non_member.allowed_to?(:move_issues)
759 projects = Project.visible.all
763 projects = Project.visible.all
760 else
764 else
761 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
765 User.current.memberships.each {|m| projects << m.project if m.roles.detect {|r| r.allowed_to?(:move_issues)}}
762 end
766 end
763 end
767 end
764 projects
768 projects
765 end
769 end
766
770
767 private
771 private
768
772
769 def update_nested_set_attributes
773 def update_nested_set_attributes
770 if root_id.nil?
774 if root_id.nil?
771 # issue was just created
775 # issue was just created
772 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
776 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
773 set_default_left_and_right
777 set_default_left_and_right
774 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
778 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
775 if @parent_issue
779 if @parent_issue
776 move_to_child_of(@parent_issue)
780 move_to_child_of(@parent_issue)
777 end
781 end
778 reload
782 reload
779 elsif parent_issue_id != parent_id
783 elsif parent_issue_id != parent_id
780 former_parent_id = parent_id
784 former_parent_id = parent_id
781 # moving an existing issue
785 # moving an existing issue
782 if @parent_issue && @parent_issue.root_id == root_id
786 if @parent_issue && @parent_issue.root_id == root_id
783 # inside the same tree
787 # inside the same tree
784 move_to_child_of(@parent_issue)
788 move_to_child_of(@parent_issue)
785 else
789 else
786 # to another tree
790 # to another tree
787 unless root?
791 unless root?
788 move_to_right_of(root)
792 move_to_right_of(root)
789 reload
793 reload
790 end
794 end
791 old_root_id = root_id
795 old_root_id = root_id
792 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
796 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
793 target_maxright = nested_set_scope.maximum(right_column_name) || 0
797 target_maxright = nested_set_scope.maximum(right_column_name) || 0
794 offset = target_maxright + 1 - lft
798 offset = target_maxright + 1 - lft
795 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
799 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
796 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
800 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
797 self[left_column_name] = lft + offset
801 self[left_column_name] = lft + offset
798 self[right_column_name] = rgt + offset
802 self[right_column_name] = rgt + offset
799 if @parent_issue
803 if @parent_issue
800 move_to_child_of(@parent_issue)
804 move_to_child_of(@parent_issue)
801 end
805 end
802 end
806 end
803 reload
807 reload
804 # delete invalid relations of all descendants
808 # delete invalid relations of all descendants
805 self_and_descendants.each do |issue|
809 self_and_descendants.each do |issue|
806 issue.relations.each do |relation|
810 issue.relations.each do |relation|
807 relation.destroy unless relation.valid?
811 relation.destroy unless relation.valid?
808 end
812 end
809 end
813 end
810 # update former parent
814 # update former parent
811 recalculate_attributes_for(former_parent_id) if former_parent_id
815 recalculate_attributes_for(former_parent_id) if former_parent_id
812 end
816 end
813 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
817 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
814 end
818 end
815
819
816 def update_parent_attributes
820 def update_parent_attributes
817 recalculate_attributes_for(parent_id) if parent_id
821 recalculate_attributes_for(parent_id) if parent_id
818 end
822 end
819
823
820 def recalculate_attributes_for(issue_id)
824 def recalculate_attributes_for(issue_id)
821 if issue_id && p = Issue.find_by_id(issue_id)
825 if issue_id && p = Issue.find_by_id(issue_id)
822 # priority = highest priority of children
826 # priority = highest priority of children
823 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
827 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :include => :priority)
824 p.priority = IssuePriority.find_by_position(priority_position)
828 p.priority = IssuePriority.find_by_position(priority_position)
825 end
829 end
826
830
827 # start/due dates = lowest/highest dates of children
831 # start/due dates = lowest/highest dates of children
828 p.start_date = p.children.minimum(:start_date)
832 p.start_date = p.children.minimum(:start_date)
829 p.due_date = p.children.maximum(:due_date)
833 p.due_date = p.children.maximum(:due_date)
830 if p.start_date && p.due_date && p.due_date < p.start_date
834 if p.start_date && p.due_date && p.due_date < p.start_date
831 p.start_date, p.due_date = p.due_date, p.start_date
835 p.start_date, p.due_date = p.due_date, p.start_date
832 end
836 end
833
837
834 # done ratio = weighted average ratio of leaves
838 # done ratio = weighted average ratio of leaves
835 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
839 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
836 leaves_count = p.leaves.count
840 leaves_count = p.leaves.count
837 if leaves_count > 0
841 if leaves_count > 0
838 average = p.leaves.average(:estimated_hours).to_f
842 average = p.leaves.average(:estimated_hours).to_f
839 if average == 0
843 if average == 0
840 average = 1
844 average = 1
841 end
845 end
842 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
846 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :include => :status).to_f
843 progress = done / (average * leaves_count)
847 progress = done / (average * leaves_count)
844 p.done_ratio = progress.round
848 p.done_ratio = progress.round
845 end
849 end
846 end
850 end
847
851
848 # estimate = sum of leaves estimates
852 # estimate = sum of leaves estimates
849 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
853 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
850 p.estimated_hours = nil if p.estimated_hours == 0.0
854 p.estimated_hours = nil if p.estimated_hours == 0.0
851
855
852 # ancestors will be recursively updated
856 # ancestors will be recursively updated
853 p.save(false)
857 p.save(false)
854 end
858 end
855 end
859 end
856
860
857 # Update issues so their versions are not pointing to a
861 # Update issues so their versions are not pointing to a
858 # fixed_version that is not shared with the issue's project
862 # fixed_version that is not shared with the issue's project
859 def self.update_versions(conditions=nil)
863 def self.update_versions(conditions=nil)
860 # Only need to update issues with a fixed_version from
864 # Only need to update issues with a fixed_version from
861 # a different project and that is not systemwide shared
865 # a different project and that is not systemwide shared
862 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
866 Issue.all(:conditions => merge_conditions("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
863 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
867 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
864 " AND #{Version.table_name}.sharing <> 'system'",
868 " AND #{Version.table_name}.sharing <> 'system'",
865 conditions),
869 conditions),
866 :include => [:project, :fixed_version]
870 :include => [:project, :fixed_version]
867 ).each do |issue|
871 ).each do |issue|
868 next if issue.project.nil? || issue.fixed_version.nil?
872 next if issue.project.nil? || issue.fixed_version.nil?
869 unless issue.project.shared_versions.include?(issue.fixed_version)
873 unless issue.project.shared_versions.include?(issue.fixed_version)
870 issue.init_journal(User.current)
874 issue.init_journal(User.current)
871 issue.fixed_version = nil
875 issue.fixed_version = nil
872 issue.save
876 issue.save
873 end
877 end
874 end
878 end
875 end
879 end
876
880
877 # Callback on attachment deletion
881 # Callback on attachment deletion
878 def attachment_added(obj)
882 def attachment_added(obj)
879 if @current_journal && !obj.new_record?
883 if @current_journal && !obj.new_record?
880 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
884 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
881 end
885 end
882 end
886 end
883
887
884 # Callback on attachment deletion
888 # Callback on attachment deletion
885 def attachment_removed(obj)
889 def attachment_removed(obj)
886 journal = init_journal(User.current)
890 journal = init_journal(User.current)
887 journal.details << JournalDetail.new(:property => 'attachment',
891 journal.details << JournalDetail.new(:property => 'attachment',
888 :prop_key => obj.id,
892 :prop_key => obj.id,
889 :old_value => obj.filename)
893 :old_value => obj.filename)
890 journal.save
894 journal.save
891 end
895 end
892
896
893 # Default assignment based on category
897 # Default assignment based on category
894 def default_assign
898 def default_assign
895 if assigned_to.nil? && category && category.assigned_to
899 if assigned_to.nil? && category && category.assigned_to
896 self.assigned_to = category.assigned_to
900 self.assigned_to = category.assigned_to
897 end
901 end
898 end
902 end
899
903
900 # Updates start/due dates of following issues
904 # Updates start/due dates of following issues
901 def reschedule_following_issues
905 def reschedule_following_issues
902 if start_date_changed? || due_date_changed?
906 if start_date_changed? || due_date_changed?
903 relations_from.each do |relation|
907 relations_from.each do |relation|
904 relation.set_issue_to_dates
908 relation.set_issue_to_dates
905 end
909 end
906 end
910 end
907 end
911 end
908
912
909 # Closes duplicates if the issue is being closed
913 # Closes duplicates if the issue is being closed
910 def close_duplicates
914 def close_duplicates
911 if closing?
915 if closing?
912 duplicates.each do |duplicate|
916 duplicates.each do |duplicate|
913 # Reload is need in case the duplicate was updated by a previous duplicate
917 # Reload is need in case the duplicate was updated by a previous duplicate
914 duplicate.reload
918 duplicate.reload
915 # Don't re-close it if it's already closed
919 # Don't re-close it if it's already closed
916 next if duplicate.closed?
920 next if duplicate.closed?
917 # Same user and notes
921 # Same user and notes
918 if @current_journal
922 if @current_journal
919 duplicate.init_journal(@current_journal.user, @current_journal.notes)
923 duplicate.init_journal(@current_journal.user, @current_journal.notes)
920 end
924 end
921 duplicate.update_attribute :status, self.status
925 duplicate.update_attribute :status, self.status
922 end
926 end
923 end
927 end
924 end
928 end
925
929
926 # Saves the changes in a Journal
930 # Saves the changes in a Journal
927 # Called after_save
931 # Called after_save
928 def create_journal
932 def create_journal
929 if @current_journal
933 if @current_journal
930 # attributes changes
934 # attributes changes
931 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
935 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
932 before = @issue_before_change.send(c)
936 before = @issue_before_change.send(c)
933 after = send(c)
937 after = send(c)
934 next if before == after || (before.blank? && after.blank?)
938 next if before == after || (before.blank? && after.blank?)
935 @current_journal.details << JournalDetail.new(:property => 'attr',
939 @current_journal.details << JournalDetail.new(:property => 'attr',
936 :prop_key => c,
940 :prop_key => c,
937 :old_value => @issue_before_change.send(c),
941 :old_value => @issue_before_change.send(c),
938 :value => send(c))
942 :value => send(c))
939 }
943 }
940 # custom fields changes
944 # custom fields changes
941 custom_values.each {|c|
945 custom_values.each {|c|
942 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
946 next if (@custom_values_before_change[c.custom_field_id]==c.value ||
943 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
947 (@custom_values_before_change[c.custom_field_id].blank? && c.value.blank?))
944 @current_journal.details << JournalDetail.new(:property => 'cf',
948 @current_journal.details << JournalDetail.new(:property => 'cf',
945 :prop_key => c.custom_field_id,
949 :prop_key => c.custom_field_id,
946 :old_value => @custom_values_before_change[c.custom_field_id],
950 :old_value => @custom_values_before_change[c.custom_field_id],
947 :value => c.value)
951 :value => c.value)
948 }
952 }
949 @current_journal.save
953 @current_journal.save
950 # reset current journal
954 # reset current journal
951 init_journal @current_journal.user, @current_journal.notes
955 init_journal @current_journal.user, @current_journal.notes
952 end
956 end
953 end
957 end
954
958
955 # Query generator for selecting groups of issue counts for a project
959 # Query generator for selecting groups of issue counts for a project
956 # based on specific criteria
960 # based on specific criteria
957 #
961 #
958 # Options
962 # Options
959 # * project - Project to search in.
963 # * project - Project to search in.
960 # * field - String. Issue field to key off of in the grouping.
964 # * field - String. Issue field to key off of in the grouping.
961 # * joins - String. The table name to join against.
965 # * joins - String. The table name to join against.
962 def self.count_and_group_by(options)
966 def self.count_and_group_by(options)
963 project = options.delete(:project)
967 project = options.delete(:project)
964 select_field = options.delete(:field)
968 select_field = options.delete(:field)
965 joins = options.delete(:joins)
969 joins = options.delete(:joins)
966
970
967 where = "#{Issue.table_name}.#{select_field}=j.id"
971 where = "#{Issue.table_name}.#{select_field}=j.id"
968
972
969 ActiveRecord::Base.connection.select_all("select s.id as status_id,
973 ActiveRecord::Base.connection.select_all("select s.id as status_id,
970 s.is_closed as closed,
974 s.is_closed as closed,
971 j.id as #{select_field},
975 j.id as #{select_field},
972 count(#{Issue.table_name}.id) as total
976 count(#{Issue.table_name}.id) as total
973 from
977 from
974 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
978 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
975 where
979 where
976 #{Issue.table_name}.status_id=s.id
980 #{Issue.table_name}.status_id=s.id
977 and #{where}
981 and #{where}
978 and #{Issue.table_name}.project_id=#{Project.table_name}.id
982 and #{Issue.table_name}.project_id=#{Project.table_name}.id
979 and #{visible_condition(User.current, :project => project)}
983 and #{visible_condition(User.current, :project => project)}
980 group by s.id, s.is_closed, j.id")
984 group by s.id, s.is_closed, j.id")
981 end
985 end
982 end
986 end
@@ -1,1159 +1,1157
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 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 :trackers, :projects_trackers,
22 :trackers, :projects_trackers,
23 :enabled_modules,
23 :enabled_modules,
24 :versions,
24 :versions,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
25 :issue_statuses, :issue_categories, :issue_relations, :workflows,
26 :enumerations,
26 :enumerations,
27 :issues,
27 :issues,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
28 :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
29 :time_entries
29 :time_entries
30
30
31 def test_create
31 def test_create
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
32 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
33 :status_id => 1, :priority => IssuePriority.all.first,
33 :status_id => 1, :priority => IssuePriority.all.first,
34 :subject => 'test_create',
34 :subject => 'test_create',
35 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
35 :description => 'IssueTest#test_create', :estimated_hours => '1:30')
36 assert issue.save
36 assert issue.save
37 issue.reload
37 issue.reload
38 assert_equal 1.5, issue.estimated_hours
38 assert_equal 1.5, issue.estimated_hours
39 end
39 end
40
40
41 def test_create_minimal
41 def test_create_minimal
42 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
42 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3,
43 :status_id => 1, :priority => IssuePriority.all.first,
43 :status_id => 1, :priority => IssuePriority.all.first,
44 :subject => 'test_create')
44 :subject => 'test_create')
45 assert issue.save
45 assert issue.save
46 assert issue.description.nil?
46 assert issue.description.nil?
47 end
47 end
48
48
49 def test_create_with_required_custom_field
49 def test_create_with_required_custom_field
50 field = IssueCustomField.find_by_name('Database')
50 field = IssueCustomField.find_by_name('Database')
51 field.update_attribute(:is_required, true)
51 field.update_attribute(:is_required, true)
52
52
53 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
53 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
54 :status_id => 1, :subject => 'test_create',
54 :status_id => 1, :subject => 'test_create',
55 :description => 'IssueTest#test_create_with_required_custom_field')
55 :description => 'IssueTest#test_create_with_required_custom_field')
56 assert issue.available_custom_fields.include?(field)
56 assert issue.available_custom_fields.include?(field)
57 # No value for the custom field
57 # No value for the custom field
58 assert !issue.save
58 assert !issue.save
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
59 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
60 issue.errors[:custom_values].to_s
60 issue.errors[:custom_values].to_s
61 # Blank value
61 # Blank value
62 issue.custom_field_values = { field.id => '' }
62 issue.custom_field_values = { field.id => '' }
63 assert !issue.save
63 assert !issue.save
64 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
64 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
65 issue.errors[:custom_values].to_s
65 issue.errors[:custom_values].to_s
66 # Invalid value
66 # Invalid value
67 issue.custom_field_values = { field.id => 'SQLServer' }
67 issue.custom_field_values = { field.id => 'SQLServer' }
68 assert !issue.save
68 assert !issue.save
69 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
69 assert_equal I18n.translate('activerecord.errors.messages.invalid'),
70 issue.errors[:custom_values].to_s
70 issue.errors[:custom_values].to_s
71 # Valid value
71 # Valid value
72 issue.custom_field_values = { field.id => 'PostgreSQL' }
72 issue.custom_field_values = { field.id => 'PostgreSQL' }
73 assert issue.save
73 assert issue.save
74 issue.reload
74 issue.reload
75 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
75 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
76 end
76 end
77
77
78 def test_create_with_group_assignment
78 def test_create_with_group_assignment
79 with_settings :issue_group_assignment => '1' do
79 with_settings :issue_group_assignment => '1' do
80 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
80 assert Issue.new(:project_id => 2, :tracker_id => 1, :author_id => 1,
81 :subject => 'Group assignment',
81 :subject => 'Group assignment',
82 :assigned_to_id => 11).save
82 :assigned_to_id => 11).save
83 issue = Issue.first(:order => 'id DESC')
83 issue = Issue.first(:order => 'id DESC')
84 assert_kind_of Group, issue.assigned_to
84 assert_kind_of Group, issue.assigned_to
85 assert_equal Group.find(11), issue.assigned_to
85 assert_equal Group.find(11), issue.assigned_to
86 end
86 end
87 end
87 end
88
88
89 def assert_visibility_match(user, issues)
89 def assert_visibility_match(user, issues)
90 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
90 assert_equal issues.collect(&:id).sort, Issue.all.select {|issue| issue.visible?(user)}.collect(&:id).sort
91 end
91 end
92
92
93 def test_visible_scope_for_anonymous
93 def test_visible_scope_for_anonymous
94 # Anonymous user should see issues of public projects only
94 # Anonymous user should see issues of public projects only
95 issues = Issue.visible(User.anonymous).all
95 issues = Issue.visible(User.anonymous).all
96 assert issues.any?
96 assert issues.any?
97 assert_nil issues.detect {|issue| !issue.project.is_public?}
97 assert_nil issues.detect {|issue| !issue.project.is_public?}
98 assert_nil issues.detect {|issue| issue.is_private?}
98 assert_nil issues.detect {|issue| issue.is_private?}
99 assert_visibility_match User.anonymous, issues
99 assert_visibility_match User.anonymous, issues
100 end
100 end
101
101
102 def test_visible_scope_for_anonymous_with_own_issues_visibility
102 def test_visible_scope_for_anonymous_with_own_issues_visibility
103 Role.anonymous.update_attribute :issues_visibility, 'own'
103 Role.anonymous.update_attribute :issues_visibility, 'own'
104 Issue.create!(:project_id => 1, :tracker_id => 1,
104 Issue.create!(:project_id => 1, :tracker_id => 1,
105 :author_id => User.anonymous.id,
105 :author_id => User.anonymous.id,
106 :subject => 'Issue by anonymous')
106 :subject => 'Issue by anonymous')
107
107
108 issues = Issue.visible(User.anonymous).all
108 issues = Issue.visible(User.anonymous).all
109 assert issues.any?
109 assert issues.any?
110 assert_nil issues.detect {|issue| issue.author != User.anonymous}
110 assert_nil issues.detect {|issue| issue.author != User.anonymous}
111 assert_visibility_match User.anonymous, issues
111 assert_visibility_match User.anonymous, issues
112 end
112 end
113
113
114 def test_visible_scope_for_anonymous_without_view_issues_permissions
114 def test_visible_scope_for_anonymous_without_view_issues_permissions
115 # Anonymous user should not see issues without permission
115 # Anonymous user should not see issues without permission
116 Role.anonymous.remove_permission!(:view_issues)
116 Role.anonymous.remove_permission!(:view_issues)
117 issues = Issue.visible(User.anonymous).all
117 issues = Issue.visible(User.anonymous).all
118 assert issues.empty?
118 assert issues.empty?
119 assert_visibility_match User.anonymous, issues
119 assert_visibility_match User.anonymous, issues
120 end
120 end
121
121
122 def test_visible_scope_for_non_member
122 def test_visible_scope_for_non_member
123 user = User.find(9)
123 user = User.find(9)
124 assert user.projects.empty?
124 assert user.projects.empty?
125 # Non member user should see issues of public projects only
125 # Non member user should see issues of public projects only
126 issues = Issue.visible(user).all
126 issues = Issue.visible(user).all
127 assert issues.any?
127 assert issues.any?
128 assert_nil issues.detect {|issue| !issue.project.is_public?}
128 assert_nil issues.detect {|issue| !issue.project.is_public?}
129 assert_nil issues.detect {|issue| issue.is_private?}
129 assert_nil issues.detect {|issue| issue.is_private?}
130 assert_visibility_match user, issues
130 assert_visibility_match user, issues
131 end
131 end
132
132
133 def test_visible_scope_for_non_member_with_own_issues_visibility
133 def test_visible_scope_for_non_member_with_own_issues_visibility
134 Role.non_member.update_attribute :issues_visibility, 'own'
134 Role.non_member.update_attribute :issues_visibility, 'own'
135 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
135 Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 9, :subject => 'Issue by non member')
136 user = User.find(9)
136 user = User.find(9)
137
137
138 issues = Issue.visible(user).all
138 issues = Issue.visible(user).all
139 assert issues.any?
139 assert issues.any?
140 assert_nil issues.detect {|issue| issue.author != user}
140 assert_nil issues.detect {|issue| issue.author != user}
141 assert_visibility_match user, issues
141 assert_visibility_match user, issues
142 end
142 end
143
143
144 def test_visible_scope_for_non_member_without_view_issues_permissions
144 def test_visible_scope_for_non_member_without_view_issues_permissions
145 # Non member user should not see issues without permission
145 # Non member user should not see issues without permission
146 Role.non_member.remove_permission!(:view_issues)
146 Role.non_member.remove_permission!(:view_issues)
147 user = User.find(9)
147 user = User.find(9)
148 assert user.projects.empty?
148 assert user.projects.empty?
149 issues = Issue.visible(user).all
149 issues = Issue.visible(user).all
150 assert issues.empty?
150 assert issues.empty?
151 assert_visibility_match user, issues
151 assert_visibility_match user, issues
152 end
152 end
153
153
154 def test_visible_scope_for_member
154 def test_visible_scope_for_member
155 user = User.find(9)
155 user = User.find(9)
156 # User should see issues of projects for which he has view_issues permissions only
156 # User should see issues of projects for which he has view_issues permissions only
157 Role.non_member.remove_permission!(:view_issues)
157 Role.non_member.remove_permission!(:view_issues)
158 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
158 Member.create!(:principal => user, :project_id => 3, :role_ids => [2])
159 issues = Issue.visible(user).all
159 issues = Issue.visible(user).all
160 assert issues.any?
160 assert issues.any?
161 assert_nil issues.detect {|issue| issue.project_id != 3}
161 assert_nil issues.detect {|issue| issue.project_id != 3}
162 assert_nil issues.detect {|issue| issue.is_private?}
162 assert_nil issues.detect {|issue| issue.is_private?}
163 assert_visibility_match user, issues
163 assert_visibility_match user, issues
164 end
164 end
165
165
166 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
166 def test_visible_scope_for_member_with_groups_should_return_assigned_issues
167 user = User.find(8)
167 user = User.find(8)
168 assert user.groups.any?
168 assert user.groups.any?
169 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
169 Member.create!(:principal => user.groups.first, :project_id => 1, :role_ids => [2])
170 Role.non_member.remove_permission!(:view_issues)
170 Role.non_member.remove_permission!(:view_issues)
171
171
172 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
172 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
173 :status_id => 1, :priority => IssuePriority.all.first,
173 :status_id => 1, :priority => IssuePriority.all.first,
174 :subject => 'Assignment test',
174 :subject => 'Assignment test',
175 :assigned_to => user.groups.first,
175 :assigned_to => user.groups.first,
176 :is_private => true)
176 :is_private => true)
177
177
178 Role.find(2).update_attribute :issues_visibility, 'default'
178 Role.find(2).update_attribute :issues_visibility, 'default'
179 issues = Issue.visible(User.find(8)).all
179 issues = Issue.visible(User.find(8)).all
180 assert issues.any?
180 assert issues.any?
181 assert issues.include?(issue)
181 assert issues.include?(issue)
182
182
183 Role.find(2).update_attribute :issues_visibility, 'own'
183 Role.find(2).update_attribute :issues_visibility, 'own'
184 issues = Issue.visible(User.find(8)).all
184 issues = Issue.visible(User.find(8)).all
185 assert issues.any?
185 assert issues.any?
186 assert issues.include?(issue)
186 assert issues.include?(issue)
187 end
187 end
188
188
189 def test_visible_scope_for_admin
189 def test_visible_scope_for_admin
190 user = User.find(1)
190 user = User.find(1)
191 user.members.each(&:destroy)
191 user.members.each(&:destroy)
192 assert user.projects.empty?
192 assert user.projects.empty?
193 issues = Issue.visible(user).all
193 issues = Issue.visible(user).all
194 assert issues.any?
194 assert issues.any?
195 # Admin should see issues on private projects that he does not belong to
195 # Admin should see issues on private projects that he does not belong to
196 assert issues.detect {|issue| !issue.project.is_public?}
196 assert issues.detect {|issue| !issue.project.is_public?}
197 # Admin should see private issues of other users
197 # Admin should see private issues of other users
198 assert issues.detect {|issue| issue.is_private? && issue.author != user}
198 assert issues.detect {|issue| issue.is_private? && issue.author != user}
199 assert_visibility_match user, issues
199 assert_visibility_match user, issues
200 end
200 end
201
201
202 def test_visible_scope_with_project
202 def test_visible_scope_with_project
203 project = Project.find(1)
203 project = Project.find(1)
204 issues = Issue.visible(User.find(2), :project => project).all
204 issues = Issue.visible(User.find(2), :project => project).all
205 projects = issues.collect(&:project).uniq
205 projects = issues.collect(&:project).uniq
206 assert_equal 1, projects.size
206 assert_equal 1, projects.size
207 assert_equal project, projects.first
207 assert_equal project, projects.first
208 end
208 end
209
209
210 def test_visible_scope_with_project_and_subprojects
210 def test_visible_scope_with_project_and_subprojects
211 project = Project.find(1)
211 project = Project.find(1)
212 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
212 issues = Issue.visible(User.find(2), :project => project, :with_subprojects => true).all
213 projects = issues.collect(&:project).uniq
213 projects = issues.collect(&:project).uniq
214 assert projects.size > 1
214 assert projects.size > 1
215 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
215 assert_equal [], projects.select {|p| !p.is_or_is_descendant_of?(project)}
216 end
216 end
217
217
218 def test_visible_and_nested_set_scopes
218 def test_visible_and_nested_set_scopes
219 assert_equal 0, Issue.find(1).descendants.visible.all.size
219 assert_equal 0, Issue.find(1).descendants.visible.all.size
220 end
220 end
221
221
222 def test_errors_full_messages_should_include_custom_fields_errors
222 def test_errors_full_messages_should_include_custom_fields_errors
223 field = IssueCustomField.find_by_name('Database')
223 field = IssueCustomField.find_by_name('Database')
224
224
225 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
225 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
226 :status_id => 1, :subject => 'test_create',
226 :status_id => 1, :subject => 'test_create',
227 :description => 'IssueTest#test_create_with_required_custom_field')
227 :description => 'IssueTest#test_create_with_required_custom_field')
228 assert issue.available_custom_fields.include?(field)
228 assert issue.available_custom_fields.include?(field)
229 # Invalid value
229 # Invalid value
230 issue.custom_field_values = { field.id => 'SQLServer' }
230 issue.custom_field_values = { field.id => 'SQLServer' }
231
231
232 assert !issue.valid?
232 assert !issue.valid?
233 assert_equal 1, issue.errors.full_messages.size
233 assert_equal 1, issue.errors.full_messages.size
234 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
234 assert_equal "Database #{I18n.translate('activerecord.errors.messages.inclusion')}",
235 issue.errors.full_messages.first
235 issue.errors.full_messages.first
236 end
236 end
237
237
238 def test_update_issue_with_required_custom_field
238 def test_update_issue_with_required_custom_field
239 field = IssueCustomField.find_by_name('Database')
239 field = IssueCustomField.find_by_name('Database')
240 field.update_attribute(:is_required, true)
240 field.update_attribute(:is_required, true)
241
241
242 issue = Issue.find(1)
242 issue = Issue.find(1)
243 assert_nil issue.custom_value_for(field)
243 assert_nil issue.custom_value_for(field)
244 assert issue.available_custom_fields.include?(field)
244 assert issue.available_custom_fields.include?(field)
245 # No change to custom values, issue can be saved
245 # No change to custom values, issue can be saved
246 assert issue.save
246 assert issue.save
247 # Blank value
247 # Blank value
248 issue.custom_field_values = { field.id => '' }
248 issue.custom_field_values = { field.id => '' }
249 assert !issue.save
249 assert !issue.save
250 # Valid value
250 # Valid value
251 issue.custom_field_values = { field.id => 'PostgreSQL' }
251 issue.custom_field_values = { field.id => 'PostgreSQL' }
252 assert issue.save
252 assert issue.save
253 issue.reload
253 issue.reload
254 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
254 assert_equal 'PostgreSQL', issue.custom_value_for(field).value
255 end
255 end
256
256
257 def test_should_not_update_attributes_if_custom_fields_validation_fails
257 def test_should_not_update_attributes_if_custom_fields_validation_fails
258 issue = Issue.find(1)
258 issue = Issue.find(1)
259 field = IssueCustomField.find_by_name('Database')
259 field = IssueCustomField.find_by_name('Database')
260 assert issue.available_custom_fields.include?(field)
260 assert issue.available_custom_fields.include?(field)
261
261
262 issue.custom_field_values = { field.id => 'Invalid' }
262 issue.custom_field_values = { field.id => 'Invalid' }
263 issue.subject = 'Should be not be saved'
263 issue.subject = 'Should be not be saved'
264 assert !issue.save
264 assert !issue.save
265
265
266 issue.reload
266 issue.reload
267 assert_equal "Can't print recipes", issue.subject
267 assert_equal "Can't print recipes", issue.subject
268 end
268 end
269
269
270 def test_should_not_recreate_custom_values_objects_on_update
270 def test_should_not_recreate_custom_values_objects_on_update
271 field = IssueCustomField.find_by_name('Database')
271 field = IssueCustomField.find_by_name('Database')
272
272
273 issue = Issue.find(1)
273 issue = Issue.find(1)
274 issue.custom_field_values = { field.id => 'PostgreSQL' }
274 issue.custom_field_values = { field.id => 'PostgreSQL' }
275 assert issue.save
275 assert issue.save
276 custom_value = issue.custom_value_for(field)
276 custom_value = issue.custom_value_for(field)
277 issue.reload
277 issue.reload
278 issue.custom_field_values = { field.id => 'MySQL' }
278 issue.custom_field_values = { field.id => 'MySQL' }
279 assert issue.save
279 assert issue.save
280 issue.reload
280 issue.reload
281 assert_equal custom_value.id, issue.custom_value_for(field).id
281 assert_equal custom_value.id, issue.custom_value_for(field).id
282 end
282 end
283
283
284 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
284 def test_should_not_update_custom_fields_on_changing_tracker_with_different_custom_fields
285 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
285 issue = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => 'Test', :custom_field_values => {'2' => 'Test'})
286 assert !Tracker.find(2).custom_field_ids.include?(2)
286 assert !Tracker.find(2).custom_field_ids.include?(2)
287
287
288 issue = Issue.find(issue.id)
288 issue = Issue.find(issue.id)
289 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
289 issue.attributes = {:tracker_id => 2, :custom_field_values => {'1' => ''}}
290
290
291 issue = Issue.find(issue.id)
291 issue = Issue.find(issue.id)
292 custom_value = issue.custom_value_for(2)
292 custom_value = issue.custom_value_for(2)
293 assert_not_nil custom_value
293 assert_not_nil custom_value
294 assert_equal 'Test', custom_value.value
294 assert_equal 'Test', custom_value.value
295 end
295 end
296
296
297 def test_assigning_tracker_id_should_reload_custom_fields_values
297 def test_assigning_tracker_id_should_reload_custom_fields_values
298 issue = Issue.new(:project => Project.find(1))
298 issue = Issue.new(:project => Project.find(1))
299 assert issue.custom_field_values.empty?
299 assert issue.custom_field_values.empty?
300 issue.tracker_id = 1
300 issue.tracker_id = 1
301 assert issue.custom_field_values.any?
301 assert issue.custom_field_values.any?
302 end
302 end
303
303
304 def test_assigning_attributes_should_assign_project_and_tracker_first
304 def test_assigning_attributes_should_assign_project_and_tracker_first
305 seq = sequence('seq')
305 seq = sequence('seq')
306 issue = Issue.new
306 issue = Issue.new
307 issue.expects(:project_id=).in_sequence(seq)
307 issue.expects(:project_id=).in_sequence(seq)
308 issue.expects(:tracker_id=).in_sequence(seq)
308 issue.expects(:tracker_id=).in_sequence(seq)
309 issue.expects(:subject=).in_sequence(seq)
309 issue.expects(:subject=).in_sequence(seq)
310 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
310 issue.attributes = {:tracker_id => 2, :project_id => 1, :subject => 'Test'}
311 end
311 end
312
312
313 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
313 def test_assigning_tracker_and_custom_fields_should_assign_custom_fields
314 attributes = ActiveSupport::OrderedHash.new
314 attributes = ActiveSupport::OrderedHash.new
315 attributes['custom_field_values'] = { '1' => 'MySQL' }
315 attributes['custom_field_values'] = { '1' => 'MySQL' }
316 attributes['tracker_id'] = '1'
316 attributes['tracker_id'] = '1'
317 issue = Issue.new(:project => Project.find(1))
317 issue = Issue.new(:project => Project.find(1))
318 issue.attributes = attributes
318 issue.attributes = attributes
319 assert_not_nil issue.custom_value_for(1)
319 assert_not_nil issue.custom_value_for(1)
320 assert_equal 'MySQL', issue.custom_value_for(1).value
320 assert_equal 'MySQL', issue.custom_value_for(1).value
321 end
321 end
322
322
323 def test_should_update_issue_with_disabled_tracker
323 def test_should_update_issue_with_disabled_tracker
324 p = Project.find(1)
324 p = Project.find(1)
325 issue = Issue.find(1)
325 issue = Issue.find(1)
326
326
327 p.trackers.delete(issue.tracker)
327 p.trackers.delete(issue.tracker)
328 assert !p.trackers.include?(issue.tracker)
328 assert !p.trackers.include?(issue.tracker)
329
329
330 issue.reload
330 issue.reload
331 issue.subject = 'New subject'
331 issue.subject = 'New subject'
332 assert issue.save
332 assert issue.save
333 end
333 end
334
334
335 def test_should_not_set_a_disabled_tracker
335 def test_should_not_set_a_disabled_tracker
336 p = Project.find(1)
336 p = Project.find(1)
337 p.trackers.delete(Tracker.find(2))
337 p.trackers.delete(Tracker.find(2))
338
338
339 issue = Issue.find(1)
339 issue = Issue.find(1)
340 issue.tracker_id = 2
340 issue.tracker_id = 2
341 issue.subject = 'New subject'
341 issue.subject = 'New subject'
342 assert !issue.save
342 assert !issue.save
343 assert_not_nil issue.errors[:tracker_id]
343 assert_not_nil issue.errors[:tracker_id]
344 end
344 end
345
345
346 def test_category_based_assignment
346 def test_category_based_assignment
347 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
347 issue = Issue.create(:project_id => 1, :tracker_id => 1, :author_id => 3,
348 :status_id => 1, :priority => IssuePriority.all.first,
348 :status_id => 1, :priority => IssuePriority.all.first,
349 :subject => 'Assignment test',
349 :subject => 'Assignment test',
350 :description => 'Assignment test', :category_id => 1)
350 :description => 'Assignment test', :category_id => 1)
351 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
351 assert_equal IssueCategory.find(1).assigned_to, issue.assigned_to
352 end
352 end
353
353
354 def test_new_statuses_allowed_to
354 def test_new_statuses_allowed_to
355 Workflow.delete_all
355 Workflow.delete_all
356
356
357 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
357 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 2, :author => false, :assignee => false)
358 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
358 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 3, :author => true, :assignee => false)
359 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
359 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 4, :author => false, :assignee => true)
360 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
360 Workflow.create!(:role_id => 1, :tracker_id => 1, :old_status_id => 1, :new_status_id => 5, :author => true, :assignee => true)
361 status = IssueStatus.find(1)
361 status = IssueStatus.find(1)
362 role = Role.find(1)
362 role = Role.find(1)
363 tracker = Tracker.find(1)
363 tracker = Tracker.find(1)
364 user = User.find(2)
364 user = User.find(2)
365
365
366 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
366 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1)
367 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
367 assert_equal [1, 2], issue.new_statuses_allowed_to(user).map(&:id)
368
368
369 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
369 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user)
370 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
370 assert_equal [1, 2, 3, 5], issue.new_statuses_allowed_to(user).map(&:id)
371
371
372 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
372 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :assigned_to => user)
373 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
373 assert_equal [1, 2, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
374
374
375 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
375 issue = Issue.generate!(:tracker => tracker, :status => status, :project_id => 1, :author => user, :assigned_to => user)
376 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
376 assert_equal [1, 2, 3, 4, 5], issue.new_statuses_allowed_to(user).map(&:id)
377 end
377 end
378
378
379 def test_copy
379 def test_copy
380 issue = Issue.new.copy_from(1)
380 issue = Issue.new.copy_from(1)
381 assert issue.save
381 assert issue.save
382 issue.reload
382 issue.reload
383 orig = Issue.find(1)
383 orig = Issue.find(1)
384 assert_equal orig.subject, issue.subject
384 assert_equal orig.subject, issue.subject
385 assert_equal orig.tracker, issue.tracker
385 assert_equal orig.tracker, issue.tracker
386 assert_equal "125", issue.custom_value_for(2).value
386 assert_equal "125", issue.custom_value_for(2).value
387 end
387 end
388
388
389 def test_copy_should_copy_status
389 def test_copy_should_copy_status
390 orig = Issue.find(8)
390 orig = Issue.find(8)
391 assert orig.status != IssueStatus.default
391 assert orig.status != IssueStatus.default
392
392
393 issue = Issue.new.copy_from(orig)
393 issue = Issue.new.copy_from(orig)
394 assert issue.save
394 assert issue.save
395 issue.reload
395 issue.reload
396 assert_equal orig.status, issue.status
396 assert_equal orig.status, issue.status
397 end
397 end
398
398
399 def test_should_close_duplicates
399 def test_should_close_duplicates
400 # Create 3 issues
400 # Create 3 issues
401 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
401 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
402 :status_id => 1, :priority => IssuePriority.all.first,
402 :status_id => 1, :priority => IssuePriority.all.first,
403 :subject => 'Duplicates test', :description => 'Duplicates test')
403 :subject => 'Duplicates test', :description => 'Duplicates test')
404 assert issue1.save
404 assert issue1.save
405 issue2 = issue1.clone
405 issue2 = issue1.clone
406 assert issue2.save
406 assert issue2.save
407 issue3 = issue1.clone
407 issue3 = issue1.clone
408 assert issue3.save
408 assert issue3.save
409
409
410 # 2 is a dupe of 1
410 # 2 is a dupe of 1
411 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
411 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
412 # And 3 is a dupe of 2
412 # And 3 is a dupe of 2
413 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
413 IssueRelation.create(:issue_from => issue3, :issue_to => issue2, :relation_type => IssueRelation::TYPE_DUPLICATES)
414 # And 3 is a dupe of 1 (circular duplicates)
414 # And 3 is a dupe of 1 (circular duplicates)
415 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
415 IssueRelation.create(:issue_from => issue3, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
416
416
417 assert issue1.reload.duplicates.include?(issue2)
417 assert issue1.reload.duplicates.include?(issue2)
418
418
419 # Closing issue 1
419 # Closing issue 1
420 issue1.init_journal(User.find(:first), "Closing issue1")
420 issue1.init_journal(User.find(:first), "Closing issue1")
421 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
421 issue1.status = IssueStatus.find :first, :conditions => {:is_closed => true}
422 assert issue1.save
422 assert issue1.save
423 # 2 and 3 should be also closed
423 # 2 and 3 should be also closed
424 assert issue2.reload.closed?
424 assert issue2.reload.closed?
425 assert issue3.reload.closed?
425 assert issue3.reload.closed?
426 end
426 end
427
427
428 def test_should_not_close_duplicated_issue
428 def test_should_not_close_duplicated_issue
429 # Create 3 issues
429 # Create 3 issues
430 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
430 issue1 = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1,
431 :status_id => 1, :priority => IssuePriority.all.first,
431 :status_id => 1, :priority => IssuePriority.all.first,
432 :subject => 'Duplicates test', :description => 'Duplicates test')
432 :subject => 'Duplicates test', :description => 'Duplicates test')
433 assert issue1.save
433 assert issue1.save
434 issue2 = issue1.clone
434 issue2 = issue1.clone
435 assert issue2.save
435 assert issue2.save
436
436
437 # 2 is a dupe of 1
437 # 2 is a dupe of 1
438 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
438 IssueRelation.create(:issue_from => issue2, :issue_to => issue1, :relation_type => IssueRelation::TYPE_DUPLICATES)
439 # 2 is a dup of 1 but 1 is not a duplicate of 2
439 # 2 is a dup of 1 but 1 is not a duplicate of 2
440 assert !issue2.reload.duplicates.include?(issue1)
440 assert !issue2.reload.duplicates.include?(issue1)
441
441
442 # Closing issue 2
442 # Closing issue 2
443 issue2.init_journal(User.find(:first), "Closing issue2")
443 issue2.init_journal(User.find(:first), "Closing issue2")
444 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
444 issue2.status = IssueStatus.find :first, :conditions => {:is_closed => true}
445 assert issue2.save
445 assert issue2.save
446 # 1 should not be also closed
446 # 1 should not be also closed
447 assert !issue1.reload.closed?
447 assert !issue1.reload.closed?
448 end
448 end
449
449
450 def test_assignable_versions
450 def test_assignable_versions
451 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
451 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
452 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
452 assert_equal ['open'], issue.assignable_versions.collect(&:status).uniq
453 end
453 end
454
454
455 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
455 def test_should_not_be_able_to_assign_a_new_issue_to_a_closed_version
456 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
456 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 1, :subject => 'New issue')
457 assert !issue.save
457 assert !issue.save
458 assert_not_nil issue.errors[:fixed_version_id]
458 assert_not_nil issue.errors[:fixed_version_id]
459 end
459 end
460
460
461 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
461 def test_should_not_be_able_to_assign_a_new_issue_to_a_locked_version
462 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
462 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 2, :subject => 'New issue')
463 assert !issue.save
463 assert !issue.save
464 assert_not_nil issue.errors[:fixed_version_id]
464 assert_not_nil issue.errors[:fixed_version_id]
465 end
465 end
466
466
467 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
467 def test_should_be_able_to_assign_a_new_issue_to_an_open_version
468 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
468 issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :fixed_version_id => 3, :subject => 'New issue')
469 assert issue.save
469 assert issue.save
470 end
470 end
471
471
472 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
472 def test_should_be_able_to_update_an_issue_assigned_to_a_closed_version
473 issue = Issue.find(11)
473 issue = Issue.find(11)
474 assert_equal 'closed', issue.fixed_version.status
474 assert_equal 'closed', issue.fixed_version.status
475 issue.subject = 'Subject changed'
475 issue.subject = 'Subject changed'
476 assert issue.save
476 assert issue.save
477 end
477 end
478
478
479 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
479 def test_should_not_be_able_to_reopen_an_issue_assigned_to_a_closed_version
480 issue = Issue.find(11)
480 issue = Issue.find(11)
481 issue.status_id = 1
481 issue.status_id = 1
482 assert !issue.save
482 assert !issue.save
483 assert_not_nil issue.errors[:base]
483 assert_not_nil issue.errors[:base]
484 end
484 end
485
485
486 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
486 def test_should_be_able_to_reopen_and_reassign_an_issue_assigned_to_a_closed_version
487 issue = Issue.find(11)
487 issue = Issue.find(11)
488 issue.status_id = 1
488 issue.status_id = 1
489 issue.fixed_version_id = 3
489 issue.fixed_version_id = 3
490 assert issue.save
490 assert issue.save
491 end
491 end
492
492
493 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
493 def test_should_be_able_to_reopen_an_issue_assigned_to_a_locked_version
494 issue = Issue.find(12)
494 issue = Issue.find(12)
495 assert_equal 'locked', issue.fixed_version.status
495 assert_equal 'locked', issue.fixed_version.status
496 issue.status_id = 1
496 issue.status_id = 1
497 assert issue.save
497 assert issue.save
498 end
498 end
499
499
500 def test_move_to_another_project_with_same_category
500 def test_move_to_another_project_with_same_category
501 issue = Issue.find(1)
501 issue = Issue.find(1)
502 assert issue.move_to_project(Project.find(2))
502 assert issue.move_to_project(Project.find(2))
503 issue.reload
503 issue.reload
504 assert_equal 2, issue.project_id
504 assert_equal 2, issue.project_id
505 # Category changes
505 # Category changes
506 assert_equal 4, issue.category_id
506 assert_equal 4, issue.category_id
507 # Make sure time entries were move to the target project
507 # Make sure time entries were move to the target project
508 assert_equal 2, issue.time_entries.first.project_id
508 assert_equal 2, issue.time_entries.first.project_id
509 end
509 end
510
510
511 def test_move_to_another_project_without_same_category
511 def test_move_to_another_project_without_same_category
512 issue = Issue.find(2)
512 issue = Issue.find(2)
513 assert issue.move_to_project(Project.find(2))
513 assert issue.move_to_project(Project.find(2))
514 issue.reload
514 issue.reload
515 assert_equal 2, issue.project_id
515 assert_equal 2, issue.project_id
516 # Category cleared
516 # Category cleared
517 assert_nil issue.category_id
517 assert_nil issue.category_id
518 end
518 end
519
519
520 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
520 def test_move_to_another_project_should_clear_fixed_version_when_not_shared
521 issue = Issue.find(1)
521 issue = Issue.find(1)
522 issue.update_attribute(:fixed_version_id, 1)
522 issue.update_attribute(:fixed_version_id, 1)
523 assert issue.move_to_project(Project.find(2))
523 assert issue.move_to_project(Project.find(2))
524 issue.reload
524 issue.reload
525 assert_equal 2, issue.project_id
525 assert_equal 2, issue.project_id
526 # Cleared fixed_version
526 # Cleared fixed_version
527 assert_equal nil, issue.fixed_version
527 assert_equal nil, issue.fixed_version
528 end
528 end
529
529
530 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
530 def test_move_to_another_project_should_keep_fixed_version_when_shared_with_the_target_project
531 issue = Issue.find(1)
531 issue = Issue.find(1)
532 issue.update_attribute(:fixed_version_id, 4)
532 issue.update_attribute(:fixed_version_id, 4)
533 assert issue.move_to_project(Project.find(5))
533 assert issue.move_to_project(Project.find(5))
534 issue.reload
534 issue.reload
535 assert_equal 5, issue.project_id
535 assert_equal 5, issue.project_id
536 # Keep fixed_version
536 # Keep fixed_version
537 assert_equal 4, issue.fixed_version_id
537 assert_equal 4, issue.fixed_version_id
538 end
538 end
539
539
540 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
540 def test_move_to_another_project_should_clear_fixed_version_when_not_shared_with_the_target_project
541 issue = Issue.find(1)
541 issue = Issue.find(1)
542 issue.update_attribute(:fixed_version_id, 1)
542 issue.update_attribute(:fixed_version_id, 1)
543 assert issue.move_to_project(Project.find(5))
543 assert issue.move_to_project(Project.find(5))
544 issue.reload
544 issue.reload
545 assert_equal 5, issue.project_id
545 assert_equal 5, issue.project_id
546 # Cleared fixed_version
546 # Cleared fixed_version
547 assert_equal nil, issue.fixed_version
547 assert_equal nil, issue.fixed_version
548 end
548 end
549
549
550 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
550 def test_move_to_another_project_should_keep_fixed_version_when_shared_systemwide
551 issue = Issue.find(1)
551 issue = Issue.find(1)
552 issue.update_attribute(:fixed_version_id, 7)
552 issue.update_attribute(:fixed_version_id, 7)
553 assert issue.move_to_project(Project.find(2))
553 assert issue.move_to_project(Project.find(2))
554 issue.reload
554 issue.reload
555 assert_equal 2, issue.project_id
555 assert_equal 2, issue.project_id
556 # Keep fixed_version
556 # Keep fixed_version
557 assert_equal 7, issue.fixed_version_id
557 assert_equal 7, issue.fixed_version_id
558 end
558 end
559
559
560 def test_move_to_another_project_with_disabled_tracker
560 def test_move_to_another_project_with_disabled_tracker
561 issue = Issue.find(1)
561 issue = Issue.find(1)
562 target = Project.find(2)
562 target = Project.find(2)
563 target.tracker_ids = [3]
563 target.tracker_ids = [3]
564 target.save
564 target.save
565 assert_equal false, issue.move_to_project(target)
565 assert_equal false, issue.move_to_project(target)
566 issue.reload
566 issue.reload
567 assert_equal 1, issue.project_id
567 assert_equal 1, issue.project_id
568 end
568 end
569
569
570 def test_copy_to_the_same_project
570 def test_copy_to_the_same_project
571 issue = Issue.find(1)
571 issue = Issue.find(1)
572 copy = nil
572 copy = nil
573 assert_difference 'Issue.count' do
573 assert_difference 'Issue.count' do
574 copy = issue.move_to_project(issue.project, nil, :copy => true)
574 copy = issue.move_to_project(issue.project, nil, :copy => true)
575 end
575 end
576 assert_kind_of Issue, copy
576 assert_kind_of Issue, copy
577 assert_equal issue.project, copy.project
577 assert_equal issue.project, copy.project
578 assert_equal "125", copy.custom_value_for(2).value
578 assert_equal "125", copy.custom_value_for(2).value
579 end
579 end
580
580
581 def test_copy_to_another_project_and_tracker
581 def test_copy_to_another_project_and_tracker
582 issue = Issue.find(1)
582 issue = Issue.find(1)
583 copy = nil
583 copy = nil
584 assert_difference 'Issue.count' do
584 assert_difference 'Issue.count' do
585 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
585 copy = issue.move_to_project(Project.find(3), Tracker.find(2), :copy => true)
586 end
586 end
587 copy.reload
587 copy.reload
588 assert_kind_of Issue, copy
588 assert_kind_of Issue, copy
589 assert_equal Project.find(3), copy.project
589 assert_equal Project.find(3), copy.project
590 assert_equal Tracker.find(2), copy.tracker
590 assert_equal Tracker.find(2), copy.tracker
591 # Custom field #2 is not associated with target tracker
591 # Custom field #2 is not associated with target tracker
592 assert_nil copy.custom_value_for(2)
592 assert_nil copy.custom_value_for(2)
593 end
593 end
594
594
595 context "#move_to_project" do
595 context "#move_to_project" do
596 context "as a copy" do
596 context "as a copy" do
597 setup do
597 setup do
598 @issue = Issue.find(1)
598 @issue = Issue.find(1)
599 @copy = nil
599 @copy = nil
600 end
600 end
601
601
602 should "not create a journal" do
602 should "not create a journal" do
603 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
603 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
604 assert_equal 0, @copy.reload.journals.size
604 assert_equal 0, @copy.reload.journals.size
605 end
605 end
606
606
607 should "allow assigned_to changes" do
607 should "allow assigned_to changes" do
608 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
608 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:assigned_to_id => 3}})
609 assert_equal 3, @copy.assigned_to_id
609 assert_equal 3, @copy.assigned_to_id
610 end
610 end
611
611
612 should "allow status changes" do
612 should "allow status changes" do
613 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
613 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:status_id => 2}})
614 assert_equal 2, @copy.status_id
614 assert_equal 2, @copy.status_id
615 end
615 end
616
616
617 should "allow start date changes" do
617 should "allow start date changes" do
618 date = Date.today
618 date = Date.today
619 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
619 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
620 assert_equal date, @copy.start_date
620 assert_equal date, @copy.start_date
621 end
621 end
622
622
623 should "allow due date changes" do
623 should "allow due date changes" do
624 date = Date.today
624 date = Date.today
625 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
625 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:due_date => date}})
626
626
627 assert_equal date, @copy.due_date
627 assert_equal date, @copy.due_date
628 end
628 end
629
629
630 should "set current user as author" do
630 should "set current user as author" do
631 User.current = User.find(9)
631 User.current = User.find(9)
632 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {}})
632 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {}})
633
633
634 assert_equal User.current, @copy.author
634 assert_equal User.current, @copy.author
635 end
635 end
636
636
637 should "keep journal notes" do
637 should "create a journal with notes" do
638 date = Date.today
638 date = Date.today
639 notes = "Notes added when copying"
639 notes = "Notes added when copying"
640 User.current = User.find(9)
640 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :notes => notes, :attributes => {:start_date => date}})
641 @issue.init_journal(User.current, notes)
642 @copy = @issue.move_to_project(Project.find(3), Tracker.find(2), {:copy => true, :attributes => {:start_date => date}})
643
641
644 assert_equal 1, @copy.journals.size
642 assert_equal 1, @copy.journals.size
645 journal = @copy.journals.first
643 journal = @copy.journals.first
646 assert_equal 0, journal.details.size
644 assert_equal 0, journal.details.size
647 assert_equal notes, journal.notes
645 assert_equal notes, journal.notes
648 end
646 end
649 end
647 end
650 end
648 end
651
649
652 def test_recipients_should_not_include_users_that_cannot_view_the_issue
650 def test_recipients_should_not_include_users_that_cannot_view_the_issue
653 issue = Issue.find(12)
651 issue = Issue.find(12)
654 assert issue.recipients.include?(issue.author.mail)
652 assert issue.recipients.include?(issue.author.mail)
655 # move the issue to a private project
653 # move the issue to a private project
656 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
654 copy = issue.move_to_project(Project.find(5), Tracker.find(2), :copy => true)
657 # author is not a member of project anymore
655 # author is not a member of project anymore
658 assert !copy.recipients.include?(copy.author.mail)
656 assert !copy.recipients.include?(copy.author.mail)
659 end
657 end
660
658
661 def test_recipients_should_include_the_assigned_group_members
659 def test_recipients_should_include_the_assigned_group_members
662 group_member = User.generate_with_protected!
660 group_member = User.generate_with_protected!
663 group = Group.generate!
661 group = Group.generate!
664 group.users << group_member
662 group.users << group_member
665
663
666 issue = Issue.find(12)
664 issue = Issue.find(12)
667 issue.assigned_to = group
665 issue.assigned_to = group
668 assert issue.recipients.include?(group_member.mail)
666 assert issue.recipients.include?(group_member.mail)
669 end
667 end
670
668
671 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
669 def test_watcher_recipients_should_not_include_users_that_cannot_view_the_issue
672 user = User.find(3)
670 user = User.find(3)
673 issue = Issue.find(9)
671 issue = Issue.find(9)
674 Watcher.create!(:user => user, :watchable => issue)
672 Watcher.create!(:user => user, :watchable => issue)
675 assert issue.watched_by?(user)
673 assert issue.watched_by?(user)
676 assert !issue.watcher_recipients.include?(user.mail)
674 assert !issue.watcher_recipients.include?(user.mail)
677 end
675 end
678
676
679 def test_issue_destroy
677 def test_issue_destroy
680 Issue.find(1).destroy
678 Issue.find(1).destroy
681 assert_nil Issue.find_by_id(1)
679 assert_nil Issue.find_by_id(1)
682 assert_nil TimeEntry.find_by_issue_id(1)
680 assert_nil TimeEntry.find_by_issue_id(1)
683 end
681 end
684
682
685 def test_blocked
683 def test_blocked
686 blocked_issue = Issue.find(9)
684 blocked_issue = Issue.find(9)
687 blocking_issue = Issue.find(10)
685 blocking_issue = Issue.find(10)
688
686
689 assert blocked_issue.blocked?
687 assert blocked_issue.blocked?
690 assert !blocking_issue.blocked?
688 assert !blocking_issue.blocked?
691 end
689 end
692
690
693 def test_blocked_issues_dont_allow_closed_statuses
691 def test_blocked_issues_dont_allow_closed_statuses
694 blocked_issue = Issue.find(9)
692 blocked_issue = Issue.find(9)
695
693
696 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
694 allowed_statuses = blocked_issue.new_statuses_allowed_to(users(:users_002))
697 assert !allowed_statuses.empty?
695 assert !allowed_statuses.empty?
698 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
696 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
699 assert closed_statuses.empty?
697 assert closed_statuses.empty?
700 end
698 end
701
699
702 def test_unblocked_issues_allow_closed_statuses
700 def test_unblocked_issues_allow_closed_statuses
703 blocking_issue = Issue.find(10)
701 blocking_issue = Issue.find(10)
704
702
705 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
703 allowed_statuses = blocking_issue.new_statuses_allowed_to(users(:users_002))
706 assert !allowed_statuses.empty?
704 assert !allowed_statuses.empty?
707 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
705 closed_statuses = allowed_statuses.select {|st| st.is_closed?}
708 assert !closed_statuses.empty?
706 assert !closed_statuses.empty?
709 end
707 end
710
708
711 def test_rescheduling_an_issue_should_reschedule_following_issue
709 def test_rescheduling_an_issue_should_reschedule_following_issue
712 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
710 issue1 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
713 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
711 issue2 = Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :status_id => 1, :subject => '-', :start_date => Date.today, :due_date => Date.today + 2)
714 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
712 IssueRelation.create!(:issue_from => issue1, :issue_to => issue2, :relation_type => IssueRelation::TYPE_PRECEDES)
715 assert_equal issue1.due_date + 1, issue2.reload.start_date
713 assert_equal issue1.due_date + 1, issue2.reload.start_date
716
714
717 issue1.due_date = Date.today + 5
715 issue1.due_date = Date.today + 5
718 issue1.save!
716 issue1.save!
719 assert_equal issue1.due_date + 1, issue2.reload.start_date
717 assert_equal issue1.due_date + 1, issue2.reload.start_date
720 end
718 end
721
719
722 def test_overdue
720 def test_overdue
723 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
721 assert Issue.new(:due_date => 1.day.ago.to_date).overdue?
724 assert !Issue.new(:due_date => Date.today).overdue?
722 assert !Issue.new(:due_date => Date.today).overdue?
725 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
723 assert !Issue.new(:due_date => 1.day.from_now.to_date).overdue?
726 assert !Issue.new(:due_date => nil).overdue?
724 assert !Issue.new(:due_date => nil).overdue?
727 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
725 assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
728 end
726 end
729
727
730 context "#behind_schedule?" do
728 context "#behind_schedule?" do
731 should "be false if the issue has no start_date" do
729 should "be false if the issue has no start_date" do
732 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
730 assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
733 end
731 end
734
732
735 should "be false if the issue has no end_date" do
733 should "be false if the issue has no end_date" do
736 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
734 assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
737 end
735 end
738
736
739 should "be false if the issue has more done than it's calendar time" do
737 should "be false if the issue has more done than it's calendar time" do
740 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
738 assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
741 end
739 end
742
740
743 should "be true if the issue hasn't been started at all" do
741 should "be true if the issue hasn't been started at all" do
744 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
742 assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
745 end
743 end
746
744
747 should "be true if the issue has used more calendar time than it's done ratio" do
745 should "be true if the issue has used more calendar time than it's done ratio" do
748 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
746 assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
749 end
747 end
750 end
748 end
751
749
752 context "#assignable_users" do
750 context "#assignable_users" do
753 should "be Users" do
751 should "be Users" do
754 assert_kind_of User, Issue.find(1).assignable_users.first
752 assert_kind_of User, Issue.find(1).assignable_users.first
755 end
753 end
756
754
757 should "include the issue author" do
755 should "include the issue author" do
758 project = Project.find(1)
756 project = Project.find(1)
759 non_project_member = User.generate!
757 non_project_member = User.generate!
760 issue = Issue.generate_for_project!(project, :author => non_project_member)
758 issue = Issue.generate_for_project!(project, :author => non_project_member)
761
759
762 assert issue.assignable_users.include?(non_project_member)
760 assert issue.assignable_users.include?(non_project_member)
763 end
761 end
764
762
765 should "include the current assignee" do
763 should "include the current assignee" do
766 project = Project.find(1)
764 project = Project.find(1)
767 user = User.generate!
765 user = User.generate!
768 issue = Issue.generate_for_project!(project, :assigned_to => user)
766 issue = Issue.generate_for_project!(project, :assigned_to => user)
769 user.lock!
767 user.lock!
770
768
771 assert Issue.find(issue.id).assignable_users.include?(user)
769 assert Issue.find(issue.id).assignable_users.include?(user)
772 end
770 end
773
771
774 should "not show the issue author twice" do
772 should "not show the issue author twice" do
775 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
773 assignable_user_ids = Issue.find(1).assignable_users.collect(&:id)
776 assert_equal 2, assignable_user_ids.length
774 assert_equal 2, assignable_user_ids.length
777
775
778 assignable_user_ids.each do |user_id|
776 assignable_user_ids.each do |user_id|
779 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
777 assert_equal 1, assignable_user_ids.select {|i| i == user_id}.length, "User #{user_id} appears more or less than once"
780 end
778 end
781 end
779 end
782
780
783 context "with issue_group_assignment" do
781 context "with issue_group_assignment" do
784 should "include groups" do
782 should "include groups" do
785 issue = Issue.new(:project => Project.find(2))
783 issue = Issue.new(:project => Project.find(2))
786
784
787 with_settings :issue_group_assignment => '1' do
785 with_settings :issue_group_assignment => '1' do
788 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
786 assert_equal %w(Group User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
789 assert issue.assignable_users.include?(Group.find(11))
787 assert issue.assignable_users.include?(Group.find(11))
790 end
788 end
791 end
789 end
792 end
790 end
793
791
794 context "without issue_group_assignment" do
792 context "without issue_group_assignment" do
795 should "not include groups" do
793 should "not include groups" do
796 issue = Issue.new(:project => Project.find(2))
794 issue = Issue.new(:project => Project.find(2))
797
795
798 with_settings :issue_group_assignment => '0' do
796 with_settings :issue_group_assignment => '0' do
799 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
797 assert_equal %w(User), issue.assignable_users.map {|a| a.class.name}.uniq.sort
800 assert !issue.assignable_users.include?(Group.find(11))
798 assert !issue.assignable_users.include?(Group.find(11))
801 end
799 end
802 end
800 end
803 end
801 end
804 end
802 end
805
803
806 def test_create_should_send_email_notification
804 def test_create_should_send_email_notification
807 ActionMailer::Base.deliveries.clear
805 ActionMailer::Base.deliveries.clear
808 issue = Issue.new(:project_id => 1, :tracker_id => 1,
806 issue = Issue.new(:project_id => 1, :tracker_id => 1,
809 :author_id => 3, :status_id => 1,
807 :author_id => 3, :status_id => 1,
810 :priority => IssuePriority.all.first,
808 :priority => IssuePriority.all.first,
811 :subject => 'test_create', :estimated_hours => '1:30')
809 :subject => 'test_create', :estimated_hours => '1:30')
812
810
813 assert issue.save
811 assert issue.save
814 assert_equal 1, ActionMailer::Base.deliveries.size
812 assert_equal 1, ActionMailer::Base.deliveries.size
815 end
813 end
816
814
817 def test_stale_issue_should_not_send_email_notification
815 def test_stale_issue_should_not_send_email_notification
818 ActionMailer::Base.deliveries.clear
816 ActionMailer::Base.deliveries.clear
819 issue = Issue.find(1)
817 issue = Issue.find(1)
820 stale = Issue.find(1)
818 stale = Issue.find(1)
821
819
822 issue.init_journal(User.find(1))
820 issue.init_journal(User.find(1))
823 issue.subject = 'Subjet update'
821 issue.subject = 'Subjet update'
824 assert issue.save
822 assert issue.save
825 assert_equal 1, ActionMailer::Base.deliveries.size
823 assert_equal 1, ActionMailer::Base.deliveries.size
826 ActionMailer::Base.deliveries.clear
824 ActionMailer::Base.deliveries.clear
827
825
828 stale.init_journal(User.find(1))
826 stale.init_journal(User.find(1))
829 stale.subject = 'Another subjet update'
827 stale.subject = 'Another subjet update'
830 assert_raise ActiveRecord::StaleObjectError do
828 assert_raise ActiveRecord::StaleObjectError do
831 stale.save
829 stale.save
832 end
830 end
833 assert ActionMailer::Base.deliveries.empty?
831 assert ActionMailer::Base.deliveries.empty?
834 end
832 end
835
833
836 def test_journalized_description
834 def test_journalized_description
837 IssueCustomField.delete_all
835 IssueCustomField.delete_all
838
836
839 i = Issue.first
837 i = Issue.first
840 old_description = i.description
838 old_description = i.description
841 new_description = "This is the new description"
839 new_description = "This is the new description"
842
840
843 i.init_journal(User.find(2))
841 i.init_journal(User.find(2))
844 i.description = new_description
842 i.description = new_description
845 assert_difference 'Journal.count', 1 do
843 assert_difference 'Journal.count', 1 do
846 assert_difference 'JournalDetail.count', 1 do
844 assert_difference 'JournalDetail.count', 1 do
847 i.save!
845 i.save!
848 end
846 end
849 end
847 end
850
848
851 detail = JournalDetail.first(:order => 'id DESC')
849 detail = JournalDetail.first(:order => 'id DESC')
852 assert_equal i, detail.journal.journalized
850 assert_equal i, detail.journal.journalized
853 assert_equal 'attr', detail.property
851 assert_equal 'attr', detail.property
854 assert_equal 'description', detail.prop_key
852 assert_equal 'description', detail.prop_key
855 assert_equal old_description, detail.old_value
853 assert_equal old_description, detail.old_value
856 assert_equal new_description, detail.value
854 assert_equal new_description, detail.value
857 end
855 end
858
856
859 def test_blank_descriptions_should_not_be_journalized
857 def test_blank_descriptions_should_not_be_journalized
860 IssueCustomField.delete_all
858 IssueCustomField.delete_all
861 Issue.update_all("description = NULL", "id=1")
859 Issue.update_all("description = NULL", "id=1")
862
860
863 i = Issue.find(1)
861 i = Issue.find(1)
864 i.init_journal(User.find(2))
862 i.init_journal(User.find(2))
865 i.subject = "blank description"
863 i.subject = "blank description"
866 i.description = "\r\n"
864 i.description = "\r\n"
867
865
868 assert_difference 'Journal.count', 1 do
866 assert_difference 'Journal.count', 1 do
869 assert_difference 'JournalDetail.count', 1 do
867 assert_difference 'JournalDetail.count', 1 do
870 i.save!
868 i.save!
871 end
869 end
872 end
870 end
873 end
871 end
874
872
875 def test_description_eol_should_be_normalized
873 def test_description_eol_should_be_normalized
876 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
874 i = Issue.new(:description => "CR \r LF \n CRLF \r\n")
877 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
875 assert_equal "CR \r\n LF \r\n CRLF \r\n", i.description
878 end
876 end
879
877
880 def test_saving_twice_should_not_duplicate_journal_details
878 def test_saving_twice_should_not_duplicate_journal_details
881 i = Issue.find(:first)
879 i = Issue.find(:first)
882 i.init_journal(User.find(2), 'Some notes')
880 i.init_journal(User.find(2), 'Some notes')
883 # initial changes
881 # initial changes
884 i.subject = 'New subject'
882 i.subject = 'New subject'
885 i.done_ratio = i.done_ratio + 10
883 i.done_ratio = i.done_ratio + 10
886 assert_difference 'Journal.count' do
884 assert_difference 'Journal.count' do
887 assert i.save
885 assert i.save
888 end
886 end
889 # 1 more change
887 # 1 more change
890 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
888 i.priority = IssuePriority.find(:first, :conditions => ["id <> ?", i.priority_id])
891 assert_no_difference 'Journal.count' do
889 assert_no_difference 'Journal.count' do
892 assert_difference 'JournalDetail.count', 1 do
890 assert_difference 'JournalDetail.count', 1 do
893 i.save
891 i.save
894 end
892 end
895 end
893 end
896 # no more change
894 # no more change
897 assert_no_difference 'Journal.count' do
895 assert_no_difference 'Journal.count' do
898 assert_no_difference 'JournalDetail.count' do
896 assert_no_difference 'JournalDetail.count' do
899 i.save
897 i.save
900 end
898 end
901 end
899 end
902 end
900 end
903
901
904 def test_all_dependent_issues
902 def test_all_dependent_issues
905 IssueRelation.delete_all
903 IssueRelation.delete_all
906 assert IssueRelation.create!(:issue_from => Issue.find(1),
904 assert IssueRelation.create!(:issue_from => Issue.find(1),
907 :issue_to => Issue.find(2),
905 :issue_to => Issue.find(2),
908 :relation_type => IssueRelation::TYPE_PRECEDES)
906 :relation_type => IssueRelation::TYPE_PRECEDES)
909 assert IssueRelation.create!(:issue_from => Issue.find(2),
907 assert IssueRelation.create!(:issue_from => Issue.find(2),
910 :issue_to => Issue.find(3),
908 :issue_to => Issue.find(3),
911 :relation_type => IssueRelation::TYPE_PRECEDES)
909 :relation_type => IssueRelation::TYPE_PRECEDES)
912 assert IssueRelation.create!(:issue_from => Issue.find(3),
910 assert IssueRelation.create!(:issue_from => Issue.find(3),
913 :issue_to => Issue.find(8),
911 :issue_to => Issue.find(8),
914 :relation_type => IssueRelation::TYPE_PRECEDES)
912 :relation_type => IssueRelation::TYPE_PRECEDES)
915
913
916 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
914 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
917 end
915 end
918
916
919 def test_all_dependent_issues_with_persistent_circular_dependency
917 def test_all_dependent_issues_with_persistent_circular_dependency
920 IssueRelation.delete_all
918 IssueRelation.delete_all
921 assert IssueRelation.create!(:issue_from => Issue.find(1),
919 assert IssueRelation.create!(:issue_from => Issue.find(1),
922 :issue_to => Issue.find(2),
920 :issue_to => Issue.find(2),
923 :relation_type => IssueRelation::TYPE_PRECEDES)
921 :relation_type => IssueRelation::TYPE_PRECEDES)
924 assert IssueRelation.create!(:issue_from => Issue.find(2),
922 assert IssueRelation.create!(:issue_from => Issue.find(2),
925 :issue_to => Issue.find(3),
923 :issue_to => Issue.find(3),
926 :relation_type => IssueRelation::TYPE_PRECEDES)
924 :relation_type => IssueRelation::TYPE_PRECEDES)
927 # Validation skipping
925 # Validation skipping
928 assert IssueRelation.new(:issue_from => Issue.find(3),
926 assert IssueRelation.new(:issue_from => Issue.find(3),
929 :issue_to => Issue.find(1),
927 :issue_to => Issue.find(1),
930 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
928 :relation_type => IssueRelation::TYPE_PRECEDES).save(false)
931
929
932 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
930 assert_equal [2, 3], Issue.find(1).all_dependent_issues.collect(&:id).sort
933 end
931 end
934
932
935 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
933 def test_all_dependent_issues_with_persistent_multiple_circular_dependencies
936 IssueRelation.delete_all
934 IssueRelation.delete_all
937 assert IssueRelation.create!(:issue_from => Issue.find(1),
935 assert IssueRelation.create!(:issue_from => Issue.find(1),
938 :issue_to => Issue.find(2),
936 :issue_to => Issue.find(2),
939 :relation_type => IssueRelation::TYPE_RELATES)
937 :relation_type => IssueRelation::TYPE_RELATES)
940 assert IssueRelation.create!(:issue_from => Issue.find(2),
938 assert IssueRelation.create!(:issue_from => Issue.find(2),
941 :issue_to => Issue.find(3),
939 :issue_to => Issue.find(3),
942 :relation_type => IssueRelation::TYPE_RELATES)
940 :relation_type => IssueRelation::TYPE_RELATES)
943 assert IssueRelation.create!(:issue_from => Issue.find(3),
941 assert IssueRelation.create!(:issue_from => Issue.find(3),
944 :issue_to => Issue.find(8),
942 :issue_to => Issue.find(8),
945 :relation_type => IssueRelation::TYPE_RELATES)
943 :relation_type => IssueRelation::TYPE_RELATES)
946 # Validation skipping
944 # Validation skipping
947 assert IssueRelation.new(:issue_from => Issue.find(8),
945 assert IssueRelation.new(:issue_from => Issue.find(8),
948 :issue_to => Issue.find(2),
946 :issue_to => Issue.find(2),
949 :relation_type => IssueRelation::TYPE_RELATES).save(false)
947 :relation_type => IssueRelation::TYPE_RELATES).save(false)
950 assert IssueRelation.new(:issue_from => Issue.find(3),
948 assert IssueRelation.new(:issue_from => Issue.find(3),
951 :issue_to => Issue.find(1),
949 :issue_to => Issue.find(1),
952 :relation_type => IssueRelation::TYPE_RELATES).save(false)
950 :relation_type => IssueRelation::TYPE_RELATES).save(false)
953
951
954 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
952 assert_equal [2, 3, 8], Issue.find(1).all_dependent_issues.collect(&:id).sort
955 end
953 end
956
954
957 context "#done_ratio" do
955 context "#done_ratio" do
958 setup do
956 setup do
959 @issue = Issue.find(1)
957 @issue = Issue.find(1)
960 @issue_status = IssueStatus.find(1)
958 @issue_status = IssueStatus.find(1)
961 @issue_status.update_attribute(:default_done_ratio, 50)
959 @issue_status.update_attribute(:default_done_ratio, 50)
962 @issue2 = Issue.find(2)
960 @issue2 = Issue.find(2)
963 @issue_status2 = IssueStatus.find(2)
961 @issue_status2 = IssueStatus.find(2)
964 @issue_status2.update_attribute(:default_done_ratio, 0)
962 @issue_status2.update_attribute(:default_done_ratio, 0)
965 end
963 end
966
964
967 context "with Setting.issue_done_ratio using the issue_field" do
965 context "with Setting.issue_done_ratio using the issue_field" do
968 setup do
966 setup do
969 Setting.issue_done_ratio = 'issue_field'
967 Setting.issue_done_ratio = 'issue_field'
970 end
968 end
971
969
972 should "read the issue's field" do
970 should "read the issue's field" do
973 assert_equal 0, @issue.done_ratio
971 assert_equal 0, @issue.done_ratio
974 assert_equal 30, @issue2.done_ratio
972 assert_equal 30, @issue2.done_ratio
975 end
973 end
976 end
974 end
977
975
978 context "with Setting.issue_done_ratio using the issue_status" do
976 context "with Setting.issue_done_ratio using the issue_status" do
979 setup do
977 setup do
980 Setting.issue_done_ratio = 'issue_status'
978 Setting.issue_done_ratio = 'issue_status'
981 end
979 end
982
980
983 should "read the Issue Status's default done ratio" do
981 should "read the Issue Status's default done ratio" do
984 assert_equal 50, @issue.done_ratio
982 assert_equal 50, @issue.done_ratio
985 assert_equal 0, @issue2.done_ratio
983 assert_equal 0, @issue2.done_ratio
986 end
984 end
987 end
985 end
988 end
986 end
989
987
990 context "#update_done_ratio_from_issue_status" do
988 context "#update_done_ratio_from_issue_status" do
991 setup do
989 setup do
992 @issue = Issue.find(1)
990 @issue = Issue.find(1)
993 @issue_status = IssueStatus.find(1)
991 @issue_status = IssueStatus.find(1)
994 @issue_status.update_attribute(:default_done_ratio, 50)
992 @issue_status.update_attribute(:default_done_ratio, 50)
995 @issue2 = Issue.find(2)
993 @issue2 = Issue.find(2)
996 @issue_status2 = IssueStatus.find(2)
994 @issue_status2 = IssueStatus.find(2)
997 @issue_status2.update_attribute(:default_done_ratio, 0)
995 @issue_status2.update_attribute(:default_done_ratio, 0)
998 end
996 end
999
997
1000 context "with Setting.issue_done_ratio using the issue_field" do
998 context "with Setting.issue_done_ratio using the issue_field" do
1001 setup do
999 setup do
1002 Setting.issue_done_ratio = 'issue_field'
1000 Setting.issue_done_ratio = 'issue_field'
1003 end
1001 end
1004
1002
1005 should "not change the issue" do
1003 should "not change the issue" do
1006 @issue.update_done_ratio_from_issue_status
1004 @issue.update_done_ratio_from_issue_status
1007 @issue2.update_done_ratio_from_issue_status
1005 @issue2.update_done_ratio_from_issue_status
1008
1006
1009 assert_equal 0, @issue.read_attribute(:done_ratio)
1007 assert_equal 0, @issue.read_attribute(:done_ratio)
1010 assert_equal 30, @issue2.read_attribute(:done_ratio)
1008 assert_equal 30, @issue2.read_attribute(:done_ratio)
1011 end
1009 end
1012 end
1010 end
1013
1011
1014 context "with Setting.issue_done_ratio using the issue_status" do
1012 context "with Setting.issue_done_ratio using the issue_status" do
1015 setup do
1013 setup do
1016 Setting.issue_done_ratio = 'issue_status'
1014 Setting.issue_done_ratio = 'issue_status'
1017 end
1015 end
1018
1016
1019 should "change the issue's done ratio" do
1017 should "change the issue's done ratio" do
1020 @issue.update_done_ratio_from_issue_status
1018 @issue.update_done_ratio_from_issue_status
1021 @issue2.update_done_ratio_from_issue_status
1019 @issue2.update_done_ratio_from_issue_status
1022
1020
1023 assert_equal 50, @issue.read_attribute(:done_ratio)
1021 assert_equal 50, @issue.read_attribute(:done_ratio)
1024 assert_equal 0, @issue2.read_attribute(:done_ratio)
1022 assert_equal 0, @issue2.read_attribute(:done_ratio)
1025 end
1023 end
1026 end
1024 end
1027 end
1025 end
1028
1026
1029 test "#by_tracker" do
1027 test "#by_tracker" do
1030 User.current = User.anonymous
1028 User.current = User.anonymous
1031 groups = Issue.by_tracker(Project.find(1))
1029 groups = Issue.by_tracker(Project.find(1))
1032 assert_equal 3, groups.size
1030 assert_equal 3, groups.size
1033 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1031 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1034 end
1032 end
1035
1033
1036 test "#by_version" do
1034 test "#by_version" do
1037 User.current = User.anonymous
1035 User.current = User.anonymous
1038 groups = Issue.by_version(Project.find(1))
1036 groups = Issue.by_version(Project.find(1))
1039 assert_equal 3, groups.size
1037 assert_equal 3, groups.size
1040 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1038 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1041 end
1039 end
1042
1040
1043 test "#by_priority" do
1041 test "#by_priority" do
1044 User.current = User.anonymous
1042 User.current = User.anonymous
1045 groups = Issue.by_priority(Project.find(1))
1043 groups = Issue.by_priority(Project.find(1))
1046 assert_equal 4, groups.size
1044 assert_equal 4, groups.size
1047 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1045 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1048 end
1046 end
1049
1047
1050 test "#by_category" do
1048 test "#by_category" do
1051 User.current = User.anonymous
1049 User.current = User.anonymous
1052 groups = Issue.by_category(Project.find(1))
1050 groups = Issue.by_category(Project.find(1))
1053 assert_equal 2, groups.size
1051 assert_equal 2, groups.size
1054 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1052 assert_equal 3, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1055 end
1053 end
1056
1054
1057 test "#by_assigned_to" do
1055 test "#by_assigned_to" do
1058 User.current = User.anonymous
1056 User.current = User.anonymous
1059 groups = Issue.by_assigned_to(Project.find(1))
1057 groups = Issue.by_assigned_to(Project.find(1))
1060 assert_equal 2, groups.size
1058 assert_equal 2, groups.size
1061 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1059 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1062 end
1060 end
1063
1061
1064 test "#by_author" do
1062 test "#by_author" do
1065 User.current = User.anonymous
1063 User.current = User.anonymous
1066 groups = Issue.by_author(Project.find(1))
1064 groups = Issue.by_author(Project.find(1))
1067 assert_equal 4, groups.size
1065 assert_equal 4, groups.size
1068 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1066 assert_equal 7, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1069 end
1067 end
1070
1068
1071 test "#by_subproject" do
1069 test "#by_subproject" do
1072 User.current = User.anonymous
1070 User.current = User.anonymous
1073 groups = Issue.by_subproject(Project.find(1))
1071 groups = Issue.by_subproject(Project.find(1))
1074 # Private descendant not visible
1072 # Private descendant not visible
1075 assert_equal 1, groups.size
1073 assert_equal 1, groups.size
1076 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1074 assert_equal 2, groups.inject(0) {|sum, group| sum + group['total'].to_i}
1077 end
1075 end
1078
1076
1079 context ".allowed_target_projects_on_move" do
1077 context ".allowed_target_projects_on_move" do
1080 should "return all active projects for admin users" do
1078 should "return all active projects for admin users" do
1081 User.current = User.find(1)
1079 User.current = User.find(1)
1082 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1080 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1083 end
1081 end
1084
1082
1085 should "return allowed projects for non admin users" do
1083 should "return allowed projects for non admin users" do
1086 User.current = User.find(2)
1084 User.current = User.find(2)
1087 Role.non_member.remove_permission! :move_issues
1085 Role.non_member.remove_permission! :move_issues
1088 assert_equal 3, Issue.allowed_target_projects_on_move.size
1086 assert_equal 3, Issue.allowed_target_projects_on_move.size
1089
1087
1090 Role.non_member.add_permission! :move_issues
1088 Role.non_member.add_permission! :move_issues
1091 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1089 assert_equal Project.active.count, Issue.allowed_target_projects_on_move.size
1092 end
1090 end
1093 end
1091 end
1094
1092
1095 def test_recently_updated_with_limit_scopes
1093 def test_recently_updated_with_limit_scopes
1096 #should return the last updated issue
1094 #should return the last updated issue
1097 assert_equal 1, Issue.recently_updated.with_limit(1).length
1095 assert_equal 1, Issue.recently_updated.with_limit(1).length
1098 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1096 assert_equal Issue.find(:first, :order => "updated_on DESC"), Issue.recently_updated.with_limit(1).first
1099 end
1097 end
1100
1098
1101 def test_on_active_projects_scope
1099 def test_on_active_projects_scope
1102 assert Project.find(2).archive
1100 assert Project.find(2).archive
1103
1101
1104 before = Issue.on_active_project.length
1102 before = Issue.on_active_project.length
1105 # test inclusion to results
1103 # test inclusion to results
1106 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1104 issue = Issue.generate_for_project!(Project.find(1), :tracker => Project.find(2).trackers.first)
1107 assert_equal before + 1, Issue.on_active_project.length
1105 assert_equal before + 1, Issue.on_active_project.length
1108
1106
1109 # Move to an archived project
1107 # Move to an archived project
1110 issue.project = Project.find(2)
1108 issue.project = Project.find(2)
1111 assert issue.save
1109 assert issue.save
1112 assert_equal before, Issue.on_active_project.length
1110 assert_equal before, Issue.on_active_project.length
1113 end
1111 end
1114
1112
1115 context "Issue#recipients" do
1113 context "Issue#recipients" do
1116 setup do
1114 setup do
1117 @project = Project.find(1)
1115 @project = Project.find(1)
1118 @author = User.generate_with_protected!
1116 @author = User.generate_with_protected!
1119 @assignee = User.generate_with_protected!
1117 @assignee = User.generate_with_protected!
1120 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1118 @issue = Issue.generate_for_project!(@project, :assigned_to => @assignee, :author => @author)
1121 end
1119 end
1122
1120
1123 should "include project recipients" do
1121 should "include project recipients" do
1124 assert @project.recipients.present?
1122 assert @project.recipients.present?
1125 @project.recipients.each do |project_recipient|
1123 @project.recipients.each do |project_recipient|
1126 assert @issue.recipients.include?(project_recipient)
1124 assert @issue.recipients.include?(project_recipient)
1127 end
1125 end
1128 end
1126 end
1129
1127
1130 should "include the author if the author is active" do
1128 should "include the author if the author is active" do
1131 assert @issue.author, "No author set for Issue"
1129 assert @issue.author, "No author set for Issue"
1132 assert @issue.recipients.include?(@issue.author.mail)
1130 assert @issue.recipients.include?(@issue.author.mail)
1133 end
1131 end
1134
1132
1135 should "include the assigned to user if the assigned to user is active" do
1133 should "include the assigned to user if the assigned to user is active" do
1136 assert @issue.assigned_to, "No assigned_to set for Issue"
1134 assert @issue.assigned_to, "No assigned_to set for Issue"
1137 assert @issue.recipients.include?(@issue.assigned_to.mail)
1135 assert @issue.recipients.include?(@issue.assigned_to.mail)
1138 end
1136 end
1139
1137
1140 should "not include users who opt out of all email" do
1138 should "not include users who opt out of all email" do
1141 @author.update_attribute(:mail_notification, :none)
1139 @author.update_attribute(:mail_notification, :none)
1142
1140
1143 assert !@issue.recipients.include?(@issue.author.mail)
1141 assert !@issue.recipients.include?(@issue.author.mail)
1144 end
1142 end
1145
1143
1146 should "not include the issue author if they are only notified of assigned issues" do
1144 should "not include the issue author if they are only notified of assigned issues" do
1147 @author.update_attribute(:mail_notification, :only_assigned)
1145 @author.update_attribute(:mail_notification, :only_assigned)
1148
1146
1149 assert !@issue.recipients.include?(@issue.author.mail)
1147 assert !@issue.recipients.include?(@issue.author.mail)
1150 end
1148 end
1151
1149
1152 should "not include the assigned user if they are only notified of owned issues" do
1150 should "not include the assigned user if they are only notified of owned issues" do
1153 @assignee.update_attribute(:mail_notification, :only_owner)
1151 @assignee.update_attribute(:mail_notification, :only_owner)
1154
1152
1155 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1153 assert !@issue.recipients.include?(@issue.assigned_to.mail)
1156 end
1154 end
1157
1155
1158 end
1156 end
1159 end
1157 end
General Comments 0
You need to be logged in to leave comments. Login now