##// END OF EJS Templates
Preload total spent time on the issue list with 1 query (#11253)....
Jean-Philippe Lang -
r14025:5e1a6e1040f2
parent child
Show More
@@ -1,1638 +1,1647
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21 include Redmine::I18n
21 include Redmine::I18n
22 before_save :set_parent_id
22 before_save :set_parent_id
23 include Redmine::NestedSet::IssueNestedSet
23 include Redmine::NestedSet::IssueNestedSet
24
24
25 belongs_to :project
25 belongs_to :project
26 belongs_to :tracker
26 belongs_to :tracker
27 belongs_to :status, :class_name => 'IssueStatus'
27 belongs_to :status, :class_name => 'IssueStatus'
28 belongs_to :author, :class_name => 'User'
28 belongs_to :author, :class_name => 'User'
29 belongs_to :assigned_to, :class_name => 'Principal'
29 belongs_to :assigned_to, :class_name => 'Principal'
30 belongs_to :fixed_version, :class_name => 'Version'
30 belongs_to :fixed_version, :class_name => 'Version'
31 belongs_to :priority, :class_name => 'IssuePriority'
31 belongs_to :priority, :class_name => 'IssuePriority'
32 belongs_to :category, :class_name => 'IssueCategory'
32 belongs_to :category, :class_name => 'IssueCategory'
33
33
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
34 has_many :journals, :as => :journalized, :dependent => :destroy, :inverse_of => :journalized
35 has_many :visible_journals,
35 has_many :visible_journals,
36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
36 lambda {where(["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false])},
37 :class_name => 'Journal',
37 :class_name => 'Journal',
38 :as => :journalized
38 :as => :journalized
39
39
40 has_many :time_entries, :dependent => :destroy
40 has_many :time_entries, :dependent => :destroy
41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
41 has_and_belongs_to_many :changesets, lambda {order("#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC")}
42
42
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45
45
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
46 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 acts_as_customizable
47 acts_as_customizable
48 acts_as_watchable
48 acts_as_watchable
49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
49 acts_as_searchable :columns => ['subject', "#{table_name}.description"],
50 :preload => [:project, :status, :tracker],
50 :preload => [:project, :status, :tracker],
51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
51 :scope => lambda {|options| options[:open_issues] ? self.open : self.all}
52
52
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
53 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
54 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
55 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56
56
57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
57 acts_as_activity_provider :scope => preload(:project, :author, :tracker),
58 :author_key => :author_id
58 :author_key => :author_id
59
59
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
60 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61
61
62 attr_reader :current_journal
62 attr_reader :current_journal
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
63 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64
64
65 validates_presence_of :subject, :project, :tracker
65 validates_presence_of :subject, :project, :tracker
66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
66 validates_presence_of :priority, :if => Proc.new {|issue| issue.new_record? || issue.priority_id_changed?}
67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
67 validates_presence_of :status, :if => Proc.new {|issue| issue.new_record? || issue.status_id_changed?}
68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
68 validates_presence_of :author, :if => Proc.new {|issue| issue.new_record? || issue.author_id_changed?}
69
69
70 validates_length_of :subject, :maximum => 255
70 validates_length_of :subject, :maximum => 255
71 validates_inclusion_of :done_ratio, :in => 0..100
71 validates_inclusion_of :done_ratio, :in => 0..100
72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
72 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
73 validates :start_date, :date => true
73 validates :start_date, :date => true
74 validates :due_date, :date => true
74 validates :due_date, :date => true
75 validate :validate_issue, :validate_required_fields
75 validate :validate_issue, :validate_required_fields
76 attr_protected :id
76 attr_protected :id
77
77
78 scope :visible, lambda {|*args|
78 scope :visible, lambda {|*args|
79 joins(:project).
79 joins(:project).
80 where(Issue.visible_condition(args.shift || User.current, *args))
80 where(Issue.visible_condition(args.shift || User.current, *args))
81 }
81 }
82
82
83 scope :open, lambda {|*args|
83 scope :open, lambda {|*args|
84 is_closed = args.size > 0 ? !args.first : false
84 is_closed = args.size > 0 ? !args.first : false
85 joins(:status).
85 joins(:status).
86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
86 where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
87 }
87 }
88
88
89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
89 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
90 scope :on_active_project, lambda {
90 scope :on_active_project, lambda {
91 joins(:project).
91 joins(:project).
92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
92 where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
93 }
93 }
94 scope :fixed_version, lambda {|versions|
94 scope :fixed_version, lambda {|versions|
95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
95 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
96 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
97 }
97 }
98
98
99 before_validation :clear_disabled_fields
99 before_validation :clear_disabled_fields
100 before_create :default_assign
100 before_create :default_assign
101 before_save :close_duplicates, :update_done_ratio_from_issue_status,
101 before_save :close_duplicates, :update_done_ratio_from_issue_status,
102 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
102 :force_updated_on_change, :update_closed_on, :set_assigned_to_was
103 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
103 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
104 after_save :reschedule_following_issues, :update_nested_set_attributes,
104 after_save :reschedule_following_issues, :update_nested_set_attributes,
105 :update_parent_attributes, :create_journal
105 :update_parent_attributes, :create_journal
106 # Should be after_create but would be called before previous after_save callbacks
106 # Should be after_create but would be called before previous after_save callbacks
107 after_save :after_create_from_copy
107 after_save :after_create_from_copy
108 after_destroy :update_parent_attributes
108 after_destroy :update_parent_attributes
109 after_create :send_notification
109 after_create :send_notification
110 # Keep it at the end of after_save callbacks
110 # Keep it at the end of after_save callbacks
111 after_save :clear_assigned_to_was
111 after_save :clear_assigned_to_was
112
112
113 # Returns a SQL conditions string used to find all issues visible by the specified user
113 # Returns a SQL conditions string used to find all issues visible by the specified user
114 def self.visible_condition(user, options={})
114 def self.visible_condition(user, options={})
115 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
115 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
116 if user.id && user.logged?
116 if user.id && user.logged?
117 case role.issues_visibility
117 case role.issues_visibility
118 when 'all'
118 when 'all'
119 nil
119 nil
120 when 'default'
120 when 'default'
121 user_ids = [user.id] + user.groups.map(&:id).compact
121 user_ids = [user.id] + user.groups.map(&:id).compact
122 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
122 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
123 when 'own'
123 when 'own'
124 user_ids = [user.id] + user.groups.map(&:id).compact
124 user_ids = [user.id] + user.groups.map(&:id).compact
125 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
125 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
126 else
126 else
127 '1=0'
127 '1=0'
128 end
128 end
129 else
129 else
130 "(#{table_name}.is_private = #{connection.quoted_false})"
130 "(#{table_name}.is_private = #{connection.quoted_false})"
131 end
131 end
132 end
132 end
133 end
133 end
134
134
135 # Returns true if usr or current user is allowed to view the issue
135 # Returns true if usr or current user is allowed to view the issue
136 def visible?(usr=nil)
136 def visible?(usr=nil)
137 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
137 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
138 if user.logged?
138 if user.logged?
139 case role.issues_visibility
139 case role.issues_visibility
140 when 'all'
140 when 'all'
141 true
141 true
142 when 'default'
142 when 'default'
143 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
143 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
144 when 'own'
144 when 'own'
145 self.author == user || user.is_or_belongs_to?(assigned_to)
145 self.author == user || user.is_or_belongs_to?(assigned_to)
146 else
146 else
147 false
147 false
148 end
148 end
149 else
149 else
150 !self.is_private?
150 !self.is_private?
151 end
151 end
152 end
152 end
153 end
153 end
154
154
155 # Returns true if user or current user is allowed to edit or add a note to the issue
155 # Returns true if user or current user is allowed to edit or add a note to the issue
156 def editable?(user=User.current)
156 def editable?(user=User.current)
157 attributes_editable?(user) || user.allowed_to?(:add_issue_notes, project)
157 attributes_editable?(user) || user.allowed_to?(:add_issue_notes, project)
158 end
158 end
159
159
160 # Returns true if user or current user is allowed to edit the issue
160 # Returns true if user or current user is allowed to edit the issue
161 def attributes_editable?(user=User.current)
161 def attributes_editable?(user=User.current)
162 user.allowed_to?(:edit_issues, project)
162 user.allowed_to?(:edit_issues, project)
163 end
163 end
164
164
165 def initialize(attributes=nil, *args)
165 def initialize(attributes=nil, *args)
166 super
166 super
167 if new_record?
167 if new_record?
168 # set default values for new records only
168 # set default values for new records only
169 self.priority ||= IssuePriority.default
169 self.priority ||= IssuePriority.default
170 self.watcher_user_ids = []
170 self.watcher_user_ids = []
171 end
171 end
172 end
172 end
173
173
174 def create_or_update
174 def create_or_update
175 super
175 super
176 ensure
176 ensure
177 @status_was = nil
177 @status_was = nil
178 end
178 end
179 private :create_or_update
179 private :create_or_update
180
180
181 # AR#Persistence#destroy would raise and RecordNotFound exception
181 # AR#Persistence#destroy would raise and RecordNotFound exception
182 # if the issue was already deleted or updated (non matching lock_version).
182 # if the issue was already deleted or updated (non matching lock_version).
183 # This is a problem when bulk deleting issues or deleting a project
183 # This is a problem when bulk deleting issues or deleting a project
184 # (because an issue may already be deleted if its parent was deleted
184 # (because an issue may already be deleted if its parent was deleted
185 # first).
185 # first).
186 # The issue is reloaded by the nested_set before being deleted so
186 # The issue is reloaded by the nested_set before being deleted so
187 # the lock_version condition should not be an issue but we handle it.
187 # the lock_version condition should not be an issue but we handle it.
188 def destroy
188 def destroy
189 super
189 super
190 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
190 rescue ActiveRecord::StaleObjectError, ActiveRecord::RecordNotFound
191 # Stale or already deleted
191 # Stale or already deleted
192 begin
192 begin
193 reload
193 reload
194 rescue ActiveRecord::RecordNotFound
194 rescue ActiveRecord::RecordNotFound
195 # The issue was actually already deleted
195 # The issue was actually already deleted
196 @destroyed = true
196 @destroyed = true
197 return freeze
197 return freeze
198 end
198 end
199 # The issue was stale, retry to destroy
199 # The issue was stale, retry to destroy
200 super
200 super
201 end
201 end
202
202
203 alias :base_reload :reload
203 alias :base_reload :reload
204 def reload(*args)
204 def reload(*args)
205 @workflow_rule_by_attribute = nil
205 @workflow_rule_by_attribute = nil
206 @assignable_versions = nil
206 @assignable_versions = nil
207 @relations = nil
207 @relations = nil
208 @spent_hours = nil
208 @spent_hours = nil
209 @total_spent_hours = nil
209 @total_spent_hours = nil
210 @total_estimated_hours = nil
210 @total_estimated_hours = nil
211 base_reload(*args)
211 base_reload(*args)
212 end
212 end
213
213
214 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
214 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
215 def available_custom_fields
215 def available_custom_fields
216 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
216 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields) : []
217 end
217 end
218
218
219 def visible_custom_field_values(user=nil)
219 def visible_custom_field_values(user=nil)
220 user_real = user || User.current
220 user_real = user || User.current
221 custom_field_values.select do |value|
221 custom_field_values.select do |value|
222 value.custom_field.visible_by?(project, user_real)
222 value.custom_field.visible_by?(project, user_real)
223 end
223 end
224 end
224 end
225
225
226 # Copies attributes from another issue, arg can be an id or an Issue
226 # Copies attributes from another issue, arg can be an id or an Issue
227 def copy_from(arg, options={})
227 def copy_from(arg, options={})
228 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
228 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
229 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
229 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
230 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
230 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
231 self.status = issue.status
231 self.status = issue.status
232 self.author = User.current
232 self.author = User.current
233 unless options[:attachments] == false
233 unless options[:attachments] == false
234 self.attachments = issue.attachments.map do |attachement|
234 self.attachments = issue.attachments.map do |attachement|
235 attachement.copy(:container => self)
235 attachement.copy(:container => self)
236 end
236 end
237 end
237 end
238 @copied_from = issue
238 @copied_from = issue
239 @copy_options = options
239 @copy_options = options
240 self
240 self
241 end
241 end
242
242
243 # Returns an unsaved copy of the issue
243 # Returns an unsaved copy of the issue
244 def copy(attributes=nil, copy_options={})
244 def copy(attributes=nil, copy_options={})
245 copy = self.class.new.copy_from(self, copy_options)
245 copy = self.class.new.copy_from(self, copy_options)
246 copy.attributes = attributes if attributes
246 copy.attributes = attributes if attributes
247 copy
247 copy
248 end
248 end
249
249
250 # Returns true if the issue is a copy
250 # Returns true if the issue is a copy
251 def copy?
251 def copy?
252 @copied_from.present?
252 @copied_from.present?
253 end
253 end
254
254
255 def status_id=(status_id)
255 def status_id=(status_id)
256 if status_id.to_s != self.status_id.to_s
256 if status_id.to_s != self.status_id.to_s
257 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
257 self.status = (status_id.present? ? IssueStatus.find_by_id(status_id) : nil)
258 end
258 end
259 self.status_id
259 self.status_id
260 end
260 end
261
261
262 # Sets the status.
262 # Sets the status.
263 def status=(status)
263 def status=(status)
264 if status != self.status
264 if status != self.status
265 @workflow_rule_by_attribute = nil
265 @workflow_rule_by_attribute = nil
266 end
266 end
267 association(:status).writer(status)
267 association(:status).writer(status)
268 end
268 end
269
269
270 def priority_id=(pid)
270 def priority_id=(pid)
271 self.priority = nil
271 self.priority = nil
272 write_attribute(:priority_id, pid)
272 write_attribute(:priority_id, pid)
273 end
273 end
274
274
275 def category_id=(cid)
275 def category_id=(cid)
276 self.category = nil
276 self.category = nil
277 write_attribute(:category_id, cid)
277 write_attribute(:category_id, cid)
278 end
278 end
279
279
280 def fixed_version_id=(vid)
280 def fixed_version_id=(vid)
281 self.fixed_version = nil
281 self.fixed_version = nil
282 write_attribute(:fixed_version_id, vid)
282 write_attribute(:fixed_version_id, vid)
283 end
283 end
284
284
285 def tracker_id=(tracker_id)
285 def tracker_id=(tracker_id)
286 if tracker_id.to_s != self.tracker_id.to_s
286 if tracker_id.to_s != self.tracker_id.to_s
287 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
287 self.tracker = (tracker_id.present? ? Tracker.find_by_id(tracker_id) : nil)
288 end
288 end
289 self.tracker_id
289 self.tracker_id
290 end
290 end
291
291
292 # Sets the tracker.
292 # Sets the tracker.
293 # This will set the status to the default status of the new tracker if:
293 # This will set the status to the default status of the new tracker if:
294 # * the status was the default for the previous tracker
294 # * the status was the default for the previous tracker
295 # * or if the status was not part of the new tracker statuses
295 # * or if the status was not part of the new tracker statuses
296 # * or the status was nil
296 # * or the status was nil
297 def tracker=(tracker)
297 def tracker=(tracker)
298 if tracker != self.tracker
298 if tracker != self.tracker
299 if status == default_status
299 if status == default_status
300 self.status = nil
300 self.status = nil
301 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
301 elsif status && tracker && !tracker.issue_status_ids.include?(status.id)
302 self.status = nil
302 self.status = nil
303 end
303 end
304 @custom_field_values = nil
304 @custom_field_values = nil
305 @workflow_rule_by_attribute = nil
305 @workflow_rule_by_attribute = nil
306 end
306 end
307 association(:tracker).writer(tracker)
307 association(:tracker).writer(tracker)
308 self.status ||= default_status
308 self.status ||= default_status
309 self.tracker
309 self.tracker
310 end
310 end
311
311
312 def project_id=(project_id)
312 def project_id=(project_id)
313 if project_id.to_s != self.project_id.to_s
313 if project_id.to_s != self.project_id.to_s
314 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
314 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
315 end
315 end
316 self.project_id
316 self.project_id
317 end
317 end
318
318
319 # Sets the project.
319 # Sets the project.
320 # Unless keep_tracker argument is set to true, this will change the tracker
320 # Unless keep_tracker argument is set to true, this will change the tracker
321 # to the first tracker of the new project if the previous tracker is not part
321 # to the first tracker of the new project if the previous tracker is not part
322 # of the new project trackers.
322 # of the new project trackers.
323 # This will clear the fixed_version is it's no longer valid for the new project.
323 # This will clear the fixed_version is it's no longer valid for the new project.
324 # This will clear the parent issue if it's no longer valid for the new project.
324 # This will clear the parent issue if it's no longer valid for the new project.
325 # This will set the category to the category with the same name in the new
325 # This will set the category to the category with the same name in the new
326 # project if it exists, or clear it if it doesn't.
326 # project if it exists, or clear it if it doesn't.
327 def project=(project, keep_tracker=false)
327 def project=(project, keep_tracker=false)
328 project_was = self.project
328 project_was = self.project
329 association(:project).writer(project)
329 association(:project).writer(project)
330 if project_was && project && project_was != project
330 if project_was && project && project_was != project
331 @assignable_versions = nil
331 @assignable_versions = nil
332
332
333 unless keep_tracker || project.trackers.include?(tracker)
333 unless keep_tracker || project.trackers.include?(tracker)
334 self.tracker = project.trackers.first
334 self.tracker = project.trackers.first
335 end
335 end
336 # Reassign to the category with same name if any
336 # Reassign to the category with same name if any
337 if category
337 if category
338 self.category = project.issue_categories.find_by_name(category.name)
338 self.category = project.issue_categories.find_by_name(category.name)
339 end
339 end
340 # Keep the fixed_version if it's still valid in the new_project
340 # Keep the fixed_version if it's still valid in the new_project
341 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
341 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
342 self.fixed_version = nil
342 self.fixed_version = nil
343 end
343 end
344 # Clear the parent task if it's no longer valid
344 # Clear the parent task if it's no longer valid
345 unless valid_parent_project?
345 unless valid_parent_project?
346 self.parent_issue_id = nil
346 self.parent_issue_id = nil
347 end
347 end
348 @custom_field_values = nil
348 @custom_field_values = nil
349 @workflow_rule_by_attribute = nil
349 @workflow_rule_by_attribute = nil
350 end
350 end
351 self.project
351 self.project
352 end
352 end
353
353
354 def description=(arg)
354 def description=(arg)
355 if arg.is_a?(String)
355 if arg.is_a?(String)
356 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
356 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
357 end
357 end
358 write_attribute(:description, arg)
358 write_attribute(:description, arg)
359 end
359 end
360
360
361 # Overrides assign_attributes so that project and tracker get assigned first
361 # Overrides assign_attributes so that project and tracker get assigned first
362 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
362 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
363 return if new_attributes.nil?
363 return if new_attributes.nil?
364 attrs = new_attributes.dup
364 attrs = new_attributes.dup
365 attrs.stringify_keys!
365 attrs.stringify_keys!
366
366
367 %w(project project_id tracker tracker_id).each do |attr|
367 %w(project project_id tracker tracker_id).each do |attr|
368 if attrs.has_key?(attr)
368 if attrs.has_key?(attr)
369 send "#{attr}=", attrs.delete(attr)
369 send "#{attr}=", attrs.delete(attr)
370 end
370 end
371 end
371 end
372 send :assign_attributes_without_project_and_tracker_first, attrs, *args
372 send :assign_attributes_without_project_and_tracker_first, attrs, *args
373 end
373 end
374 # Do not redefine alias chain on reload (see #4838)
374 # Do not redefine alias chain on reload (see #4838)
375 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
375 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
376
376
377 def attributes=(new_attributes)
377 def attributes=(new_attributes)
378 assign_attributes new_attributes
378 assign_attributes new_attributes
379 end
379 end
380
380
381 def estimated_hours=(h)
381 def estimated_hours=(h)
382 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
382 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
383 end
383 end
384
384
385 safe_attributes 'project_id',
385 safe_attributes 'project_id',
386 'tracker_id',
386 'tracker_id',
387 'status_id',
387 'status_id',
388 'category_id',
388 'category_id',
389 'assigned_to_id',
389 'assigned_to_id',
390 'priority_id',
390 'priority_id',
391 'fixed_version_id',
391 'fixed_version_id',
392 'subject',
392 'subject',
393 'description',
393 'description',
394 'start_date',
394 'start_date',
395 'due_date',
395 'due_date',
396 'done_ratio',
396 'done_ratio',
397 'estimated_hours',
397 'estimated_hours',
398 'custom_field_values',
398 'custom_field_values',
399 'custom_fields',
399 'custom_fields',
400 'lock_version',
400 'lock_version',
401 'notes',
401 'notes',
402 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
402 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
403
403
404 safe_attributes 'notes',
404 safe_attributes 'notes',
405 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
405 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
406
406
407 safe_attributes 'private_notes',
407 safe_attributes 'private_notes',
408 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
408 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
409
409
410 safe_attributes 'watcher_user_ids',
410 safe_attributes 'watcher_user_ids',
411 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
411 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
412
412
413 safe_attributes 'is_private',
413 safe_attributes 'is_private',
414 :if => lambda {|issue, user|
414 :if => lambda {|issue, user|
415 user.allowed_to?(:set_issues_private, issue.project) ||
415 user.allowed_to?(:set_issues_private, issue.project) ||
416 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
416 (issue.author_id == user.id && user.allowed_to?(:set_own_issues_private, issue.project))
417 }
417 }
418
418
419 safe_attributes 'parent_issue_id',
419 safe_attributes 'parent_issue_id',
420 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
420 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
421 user.allowed_to?(:manage_subtasks, issue.project)}
421 user.allowed_to?(:manage_subtasks, issue.project)}
422
422
423 def safe_attribute_names(user=nil)
423 def safe_attribute_names(user=nil)
424 names = super
424 names = super
425 names -= disabled_core_fields
425 names -= disabled_core_fields
426 names -= read_only_attribute_names(user)
426 names -= read_only_attribute_names(user)
427 if new_record?
427 if new_record?
428 # Make sure that project_id can always be set for new issues
428 # Make sure that project_id can always be set for new issues
429 names |= %w(project_id)
429 names |= %w(project_id)
430 end
430 end
431 if dates_derived?
431 if dates_derived?
432 names -= %w(start_date due_date)
432 names -= %w(start_date due_date)
433 end
433 end
434 if priority_derived?
434 if priority_derived?
435 names -= %w(priority_id)
435 names -= %w(priority_id)
436 end
436 end
437 if done_ratio_derived?
437 if done_ratio_derived?
438 names -= %w(done_ratio)
438 names -= %w(done_ratio)
439 end
439 end
440 names
440 names
441 end
441 end
442
442
443 # Safely sets attributes
443 # Safely sets attributes
444 # Should be called from controllers instead of #attributes=
444 # Should be called from controllers instead of #attributes=
445 # attr_accessible is too rough because we still want things like
445 # attr_accessible is too rough because we still want things like
446 # Issue.new(:project => foo) to work
446 # Issue.new(:project => foo) to work
447 def safe_attributes=(attrs, user=User.current)
447 def safe_attributes=(attrs, user=User.current)
448 return unless attrs.is_a?(Hash)
448 return unless attrs.is_a?(Hash)
449
449
450 attrs = attrs.deep_dup
450 attrs = attrs.deep_dup
451
451
452 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
452 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
453 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
453 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
454 if allowed_target_projects(user).where(:id => p.to_i).exists?
454 if allowed_target_projects(user).where(:id => p.to_i).exists?
455 self.project_id = p
455 self.project_id = p
456 end
456 end
457 end
457 end
458
458
459 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
459 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
460 self.tracker_id = t
460 self.tracker_id = t
461 end
461 end
462 if project
462 if project
463 # Set the default tracker to accept custom field values
463 # Set the default tracker to accept custom field values
464 # even if tracker is not specified
464 # even if tracker is not specified
465 self.tracker ||= project.trackers.first
465 self.tracker ||= project.trackers.first
466 end
466 end
467
467
468 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
468 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
469 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
469 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
470 self.status_id = s
470 self.status_id = s
471 end
471 end
472 end
472 end
473
473
474 attrs = delete_unsafe_attributes(attrs, user)
474 attrs = delete_unsafe_attributes(attrs, user)
475 return if attrs.empty?
475 return if attrs.empty?
476
476
477 if attrs['parent_issue_id'].present?
477 if attrs['parent_issue_id'].present?
478 s = attrs['parent_issue_id'].to_s
478 s = attrs['parent_issue_id'].to_s
479 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
479 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
480 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
480 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
481 end
481 end
482 end
482 end
483
483
484 if attrs['custom_field_values'].present?
484 if attrs['custom_field_values'].present?
485 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
485 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
486 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
486 attrs['custom_field_values'].select! {|k, v| editable_custom_field_ids.include?(k.to_s)}
487 end
487 end
488
488
489 if attrs['custom_fields'].present?
489 if attrs['custom_fields'].present?
490 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
490 editable_custom_field_ids = editable_custom_field_values(user).map {|v| v.custom_field_id.to_s}
491 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
491 attrs['custom_fields'].select! {|c| editable_custom_field_ids.include?(c['id'].to_s)}
492 end
492 end
493
493
494 # mass-assignment security bypass
494 # mass-assignment security bypass
495 assign_attributes attrs, :without_protection => true
495 assign_attributes attrs, :without_protection => true
496 end
496 end
497
497
498 def disabled_core_fields
498 def disabled_core_fields
499 tracker ? tracker.disabled_core_fields : []
499 tracker ? tracker.disabled_core_fields : []
500 end
500 end
501
501
502 # Returns the custom_field_values that can be edited by the given user
502 # Returns the custom_field_values that can be edited by the given user
503 def editable_custom_field_values(user=nil)
503 def editable_custom_field_values(user=nil)
504 visible_custom_field_values(user).reject do |value|
504 visible_custom_field_values(user).reject do |value|
505 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
505 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
506 end
506 end
507 end
507 end
508
508
509 # Returns the custom fields that can be edited by the given user
509 # Returns the custom fields that can be edited by the given user
510 def editable_custom_fields(user=nil)
510 def editable_custom_fields(user=nil)
511 editable_custom_field_values(user).map(&:custom_field).uniq
511 editable_custom_field_values(user).map(&:custom_field).uniq
512 end
512 end
513
513
514 # Returns the names of attributes that are read-only for user or the current user
514 # Returns the names of attributes that are read-only for user or the current user
515 # For users with multiple roles, the read-only fields are the intersection of
515 # For users with multiple roles, the read-only fields are the intersection of
516 # read-only fields of each role
516 # read-only fields of each role
517 # The result is an array of strings where sustom fields are represented with their ids
517 # The result is an array of strings where sustom fields are represented with their ids
518 #
518 #
519 # Examples:
519 # Examples:
520 # issue.read_only_attribute_names # => ['due_date', '2']
520 # issue.read_only_attribute_names # => ['due_date', '2']
521 # issue.read_only_attribute_names(user) # => []
521 # issue.read_only_attribute_names(user) # => []
522 def read_only_attribute_names(user=nil)
522 def read_only_attribute_names(user=nil)
523 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
523 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
524 end
524 end
525
525
526 # Returns the names of required attributes for user or the current user
526 # Returns the names of required attributes for user or the current user
527 # For users with multiple roles, the required fields are the intersection of
527 # For users with multiple roles, the required fields are the intersection of
528 # required fields of each role
528 # required fields of each role
529 # The result is an array of strings where sustom fields are represented with their ids
529 # The result is an array of strings where sustom fields are represented with their ids
530 #
530 #
531 # Examples:
531 # Examples:
532 # issue.required_attribute_names # => ['due_date', '2']
532 # issue.required_attribute_names # => ['due_date', '2']
533 # issue.required_attribute_names(user) # => []
533 # issue.required_attribute_names(user) # => []
534 def required_attribute_names(user=nil)
534 def required_attribute_names(user=nil)
535 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
535 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
536 end
536 end
537
537
538 # Returns true if the attribute is required for user
538 # Returns true if the attribute is required for user
539 def required_attribute?(name, user=nil)
539 def required_attribute?(name, user=nil)
540 required_attribute_names(user).include?(name.to_s)
540 required_attribute_names(user).include?(name.to_s)
541 end
541 end
542
542
543 # Returns a hash of the workflow rule by attribute for the given user
543 # Returns a hash of the workflow rule by attribute for the given user
544 #
544 #
545 # Examples:
545 # Examples:
546 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
546 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
547 def workflow_rule_by_attribute(user=nil)
547 def workflow_rule_by_attribute(user=nil)
548 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
548 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
549
549
550 user_real = user || User.current
550 user_real = user || User.current
551 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
551 roles = user_real.admin ? Role.all.to_a : user_real.roles_for_project(project)
552 roles = roles.select(&:consider_workflow?)
552 roles = roles.select(&:consider_workflow?)
553 return {} if roles.empty?
553 return {} if roles.empty?
554
554
555 result = {}
555 result = {}
556 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
556 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).to_a
557 if workflow_permissions.any?
557 if workflow_permissions.any?
558 workflow_rules = workflow_permissions.inject({}) do |h, wp|
558 workflow_rules = workflow_permissions.inject({}) do |h, wp|
559 h[wp.field_name] ||= {}
559 h[wp.field_name] ||= {}
560 h[wp.field_name][wp.role_id] = wp.rule
560 h[wp.field_name][wp.role_id] = wp.rule
561 h
561 h
562 end
562 end
563 fields_with_roles = {}
563 fields_with_roles = {}
564 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
564 IssueCustomField.where(:visible => false).joins(:roles).pluck(:id, "role_id").each do |field_id, role_id|
565 fields_with_roles[field_id] ||= []
565 fields_with_roles[field_id] ||= []
566 fields_with_roles[field_id] << role_id
566 fields_with_roles[field_id] << role_id
567 end
567 end
568 roles.each do |role|
568 roles.each do |role|
569 fields_with_roles.each do |field_id, role_ids|
569 fields_with_roles.each do |field_id, role_ids|
570 unless role_ids.include?(role.id)
570 unless role_ids.include?(role.id)
571 field_name = field_id.to_s
571 field_name = field_id.to_s
572 workflow_rules[field_name] ||= {}
572 workflow_rules[field_name] ||= {}
573 workflow_rules[field_name][role.id] = 'readonly'
573 workflow_rules[field_name][role.id] = 'readonly'
574 end
574 end
575 end
575 end
576 end
576 end
577 workflow_rules.each do |attr, rules|
577 workflow_rules.each do |attr, rules|
578 next if rules.size < roles.size
578 next if rules.size < roles.size
579 uniq_rules = rules.values.uniq
579 uniq_rules = rules.values.uniq
580 if uniq_rules.size == 1
580 if uniq_rules.size == 1
581 result[attr] = uniq_rules.first
581 result[attr] = uniq_rules.first
582 else
582 else
583 result[attr] = 'required'
583 result[attr] = 'required'
584 end
584 end
585 end
585 end
586 end
586 end
587 @workflow_rule_by_attribute = result if user.nil?
587 @workflow_rule_by_attribute = result if user.nil?
588 result
588 result
589 end
589 end
590 private :workflow_rule_by_attribute
590 private :workflow_rule_by_attribute
591
591
592 def done_ratio
592 def done_ratio
593 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
593 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
594 status.default_done_ratio
594 status.default_done_ratio
595 else
595 else
596 read_attribute(:done_ratio)
596 read_attribute(:done_ratio)
597 end
597 end
598 end
598 end
599
599
600 def self.use_status_for_done_ratio?
600 def self.use_status_for_done_ratio?
601 Setting.issue_done_ratio == 'issue_status'
601 Setting.issue_done_ratio == 'issue_status'
602 end
602 end
603
603
604 def self.use_field_for_done_ratio?
604 def self.use_field_for_done_ratio?
605 Setting.issue_done_ratio == 'issue_field'
605 Setting.issue_done_ratio == 'issue_field'
606 end
606 end
607
607
608 def validate_issue
608 def validate_issue
609 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
609 if due_date && start_date && (start_date_changed? || due_date_changed?) && due_date < start_date
610 errors.add :due_date, :greater_than_start_date
610 errors.add :due_date, :greater_than_start_date
611 end
611 end
612
612
613 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
613 if start_date && start_date_changed? && soonest_start && start_date < soonest_start
614 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
614 errors.add :start_date, :earlier_than_minimum_start_date, :date => format_date(soonest_start)
615 end
615 end
616
616
617 if fixed_version
617 if fixed_version
618 if !assignable_versions.include?(fixed_version)
618 if !assignable_versions.include?(fixed_version)
619 errors.add :fixed_version_id, :inclusion
619 errors.add :fixed_version_id, :inclusion
620 elsif reopening? && fixed_version.closed?
620 elsif reopening? && fixed_version.closed?
621 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
621 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
622 end
622 end
623 end
623 end
624
624
625 # Checks that the issue can not be added/moved to a disabled tracker
625 # Checks that the issue can not be added/moved to a disabled tracker
626 if project && (tracker_id_changed? || project_id_changed?)
626 if project && (tracker_id_changed? || project_id_changed?)
627 unless project.trackers.include?(tracker)
627 unless project.trackers.include?(tracker)
628 errors.add :tracker_id, :inclusion
628 errors.add :tracker_id, :inclusion
629 end
629 end
630 end
630 end
631
631
632 # Checks parent issue assignment
632 # Checks parent issue assignment
633 if @invalid_parent_issue_id.present?
633 if @invalid_parent_issue_id.present?
634 errors.add :parent_issue_id, :invalid
634 errors.add :parent_issue_id, :invalid
635 elsif @parent_issue
635 elsif @parent_issue
636 if !valid_parent_project?(@parent_issue)
636 if !valid_parent_project?(@parent_issue)
637 errors.add :parent_issue_id, :invalid
637 errors.add :parent_issue_id, :invalid
638 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
638 elsif (@parent_issue != parent) && (all_dependent_issues.include?(@parent_issue) || @parent_issue.all_dependent_issues.include?(self))
639 errors.add :parent_issue_id, :invalid
639 errors.add :parent_issue_id, :invalid
640 elsif !new_record?
640 elsif !new_record?
641 # moving an existing issue
641 # moving an existing issue
642 if move_possible?(@parent_issue)
642 if move_possible?(@parent_issue)
643 # move accepted
643 # move accepted
644 else
644 else
645 errors.add :parent_issue_id, :invalid
645 errors.add :parent_issue_id, :invalid
646 end
646 end
647 end
647 end
648 end
648 end
649 end
649 end
650
650
651 # Validates the issue against additional workflow requirements
651 # Validates the issue against additional workflow requirements
652 def validate_required_fields
652 def validate_required_fields
653 user = new_record? ? author : current_journal.try(:user)
653 user = new_record? ? author : current_journal.try(:user)
654
654
655 required_attribute_names(user).each do |attribute|
655 required_attribute_names(user).each do |attribute|
656 if attribute =~ /^\d+$/
656 if attribute =~ /^\d+$/
657 attribute = attribute.to_i
657 attribute = attribute.to_i
658 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
658 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
659 if v && v.value.blank?
659 if v && v.value.blank?
660 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
660 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
661 end
661 end
662 else
662 else
663 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
663 if respond_to?(attribute) && send(attribute).blank? && !disabled_core_fields.include?(attribute)
664 errors.add attribute, :blank
664 errors.add attribute, :blank
665 end
665 end
666 end
666 end
667 end
667 end
668 end
668 end
669
669
670 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
670 # Overrides Redmine::Acts::Customizable::InstanceMethods#validate_custom_field_values
671 # so that custom values that are not editable are not validated (eg. a custom field that
671 # so that custom values that are not editable are not validated (eg. a custom field that
672 # is marked as required should not trigger a validation error if the user is not allowed
672 # is marked as required should not trigger a validation error if the user is not allowed
673 # to edit this field).
673 # to edit this field).
674 def validate_custom_field_values
674 def validate_custom_field_values
675 user = new_record? ? author : current_journal.try(:user)
675 user = new_record? ? author : current_journal.try(:user)
676 if new_record? || custom_field_values_changed?
676 if new_record? || custom_field_values_changed?
677 editable_custom_field_values(user).each(&:validate_value)
677 editable_custom_field_values(user).each(&:validate_value)
678 end
678 end
679 end
679 end
680
680
681 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
681 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
682 # even if the user turns off the setting later
682 # even if the user turns off the setting later
683 def update_done_ratio_from_issue_status
683 def update_done_ratio_from_issue_status
684 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
684 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
685 self.done_ratio = status.default_done_ratio
685 self.done_ratio = status.default_done_ratio
686 end
686 end
687 end
687 end
688
688
689 def init_journal(user, notes = "")
689 def init_journal(user, notes = "")
690 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
690 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
691 end
691 end
692
692
693 # Returns the current journal or nil if it's not initialized
693 # Returns the current journal or nil if it's not initialized
694 def current_journal
694 def current_journal
695 @current_journal
695 @current_journal
696 end
696 end
697
697
698 # Returns the names of attributes that are journalized when updating the issue
698 # Returns the names of attributes that are journalized when updating the issue
699 def journalized_attribute_names
699 def journalized_attribute_names
700 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
700 names = Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)
701 if tracker
701 if tracker
702 names -= tracker.disabled_core_fields
702 names -= tracker.disabled_core_fields
703 end
703 end
704 names
704 names
705 end
705 end
706
706
707 # Returns the id of the last journal or nil
707 # Returns the id of the last journal or nil
708 def last_journal_id
708 def last_journal_id
709 if new_record?
709 if new_record?
710 nil
710 nil
711 else
711 else
712 journals.maximum(:id)
712 journals.maximum(:id)
713 end
713 end
714 end
714 end
715
715
716 # Returns a scope for journals that have an id greater than journal_id
716 # Returns a scope for journals that have an id greater than journal_id
717 def journals_after(journal_id)
717 def journals_after(journal_id)
718 scope = journals.reorder("#{Journal.table_name}.id ASC")
718 scope = journals.reorder("#{Journal.table_name}.id ASC")
719 if journal_id.present?
719 if journal_id.present?
720 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
720 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
721 end
721 end
722 scope
722 scope
723 end
723 end
724
724
725 # Returns the initial status of the issue
725 # Returns the initial status of the issue
726 # Returns nil for a new issue
726 # Returns nil for a new issue
727 def status_was
727 def status_was
728 if status_id_changed?
728 if status_id_changed?
729 if status_id_was.to_i > 0
729 if status_id_was.to_i > 0
730 @status_was ||= IssueStatus.find_by_id(status_id_was)
730 @status_was ||= IssueStatus.find_by_id(status_id_was)
731 end
731 end
732 else
732 else
733 @status_was ||= status
733 @status_was ||= status
734 end
734 end
735 end
735 end
736
736
737 # Return true if the issue is closed, otherwise false
737 # Return true if the issue is closed, otherwise false
738 def closed?
738 def closed?
739 status.present? && status.is_closed?
739 status.present? && status.is_closed?
740 end
740 end
741
741
742 # Returns true if the issue was closed when loaded
742 # Returns true if the issue was closed when loaded
743 def was_closed?
743 def was_closed?
744 status_was.present? && status_was.is_closed?
744 status_was.present? && status_was.is_closed?
745 end
745 end
746
746
747 # Return true if the issue is being reopened
747 # Return true if the issue is being reopened
748 def reopening?
748 def reopening?
749 if new_record?
749 if new_record?
750 false
750 false
751 else
751 else
752 status_id_changed? && !closed? && was_closed?
752 status_id_changed? && !closed? && was_closed?
753 end
753 end
754 end
754 end
755 alias :reopened? :reopening?
755 alias :reopened? :reopening?
756
756
757 # Return true if the issue is being closed
757 # Return true if the issue is being closed
758 def closing?
758 def closing?
759 if new_record?
759 if new_record?
760 closed?
760 closed?
761 else
761 else
762 status_id_changed? && closed? && !was_closed?
762 status_id_changed? && closed? && !was_closed?
763 end
763 end
764 end
764 end
765
765
766 # Returns true if the issue is overdue
766 # Returns true if the issue is overdue
767 def overdue?
767 def overdue?
768 due_date.present? && (due_date < Date.today) && !closed?
768 due_date.present? && (due_date < Date.today) && !closed?
769 end
769 end
770
770
771 # Is the amount of work done less than it should for the due date
771 # Is the amount of work done less than it should for the due date
772 def behind_schedule?
772 def behind_schedule?
773 return false if start_date.nil? || due_date.nil?
773 return false if start_date.nil? || due_date.nil?
774 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
774 done_date = start_date + ((due_date - start_date + 1) * done_ratio / 100).floor
775 return done_date <= Date.today
775 return done_date <= Date.today
776 end
776 end
777
777
778 # Does this issue have children?
778 # Does this issue have children?
779 def children?
779 def children?
780 !leaf?
780 !leaf?
781 end
781 end
782
782
783 # Users the issue can be assigned to
783 # Users the issue can be assigned to
784 def assignable_users
784 def assignable_users
785 users = project.assignable_users.to_a
785 users = project.assignable_users.to_a
786 users << author if author
786 users << author if author
787 users << assigned_to if assigned_to
787 users << assigned_to if assigned_to
788 users.uniq.sort
788 users.uniq.sort
789 end
789 end
790
790
791 # Versions that the issue can be assigned to
791 # Versions that the issue can be assigned to
792 def assignable_versions
792 def assignable_versions
793 return @assignable_versions if @assignable_versions
793 return @assignable_versions if @assignable_versions
794
794
795 versions = project.shared_versions.open.to_a
795 versions = project.shared_versions.open.to_a
796 if fixed_version
796 if fixed_version
797 if fixed_version_id_changed?
797 if fixed_version_id_changed?
798 # nothing to do
798 # nothing to do
799 elsif project_id_changed?
799 elsif project_id_changed?
800 if project.shared_versions.include?(fixed_version)
800 if project.shared_versions.include?(fixed_version)
801 versions << fixed_version
801 versions << fixed_version
802 end
802 end
803 else
803 else
804 versions << fixed_version
804 versions << fixed_version
805 end
805 end
806 end
806 end
807 @assignable_versions = versions.uniq.sort
807 @assignable_versions = versions.uniq.sort
808 end
808 end
809
809
810 # Returns true if this issue is blocked by another issue that is still open
810 # Returns true if this issue is blocked by another issue that is still open
811 def blocked?
811 def blocked?
812 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
812 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
813 end
813 end
814
814
815 # Returns the default status of the issue based on its tracker
815 # Returns the default status of the issue based on its tracker
816 # Returns nil if tracker is nil
816 # Returns nil if tracker is nil
817 def default_status
817 def default_status
818 tracker.try(:default_status)
818 tracker.try(:default_status)
819 end
819 end
820
820
821 # Returns an array of statuses that user is able to apply
821 # Returns an array of statuses that user is able to apply
822 def new_statuses_allowed_to(user=User.current, include_default=false)
822 def new_statuses_allowed_to(user=User.current, include_default=false)
823 if new_record? && @copied_from
823 if new_record? && @copied_from
824 [default_status, @copied_from.status].compact.uniq.sort
824 [default_status, @copied_from.status].compact.uniq.sort
825 else
825 else
826 initial_status = nil
826 initial_status = nil
827 if new_record?
827 if new_record?
828 initial_status = default_status
828 initial_status = default_status
829 elsif tracker_id_changed?
829 elsif tracker_id_changed?
830 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
830 if Tracker.where(:id => tracker_id_was, :default_status_id => status_id_was).any?
831 initial_status = default_status
831 initial_status = default_status
832 elsif tracker.issue_status_ids.include?(status_id_was)
832 elsif tracker.issue_status_ids.include?(status_id_was)
833 initial_status = IssueStatus.find_by_id(status_id_was)
833 initial_status = IssueStatus.find_by_id(status_id_was)
834 else
834 else
835 initial_status = default_status
835 initial_status = default_status
836 end
836 end
837 else
837 else
838 initial_status = status_was
838 initial_status = status_was
839 end
839 end
840
840
841 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
841 initial_assigned_to_id = assigned_to_id_changed? ? assigned_to_id_was : assigned_to_id
842 assignee_transitions_allowed = initial_assigned_to_id.present? &&
842 assignee_transitions_allowed = initial_assigned_to_id.present? &&
843 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
843 (user.id == initial_assigned_to_id || user.group_ids.include?(initial_assigned_to_id))
844
844
845 statuses = []
845 statuses = []
846 if initial_status
846 if initial_status
847 statuses += initial_status.find_new_statuses_allowed_to(
847 statuses += initial_status.find_new_statuses_allowed_to(
848 user.admin ? Role.all.to_a : user.roles_for_project(project),
848 user.admin ? Role.all.to_a : user.roles_for_project(project),
849 tracker,
849 tracker,
850 author == user,
850 author == user,
851 assignee_transitions_allowed
851 assignee_transitions_allowed
852 )
852 )
853 end
853 end
854 statuses << initial_status unless statuses.empty?
854 statuses << initial_status unless statuses.empty?
855 statuses << default_status if include_default
855 statuses << default_status if include_default
856 statuses = statuses.compact.uniq.sort
856 statuses = statuses.compact.uniq.sort
857 if blocked?
857 if blocked?
858 statuses.reject!(&:is_closed?)
858 statuses.reject!(&:is_closed?)
859 end
859 end
860 statuses
860 statuses
861 end
861 end
862 end
862 end
863
863
864 # Returns the previous assignee (user or group) if changed
864 # Returns the previous assignee (user or group) if changed
865 def assigned_to_was
865 def assigned_to_was
866 # assigned_to_id_was is reset before after_save callbacks
866 # assigned_to_id_was is reset before after_save callbacks
867 user_id = @previous_assigned_to_id || assigned_to_id_was
867 user_id = @previous_assigned_to_id || assigned_to_id_was
868 if user_id && user_id != assigned_to_id
868 if user_id && user_id != assigned_to_id
869 @assigned_to_was ||= Principal.find_by_id(user_id)
869 @assigned_to_was ||= Principal.find_by_id(user_id)
870 end
870 end
871 end
871 end
872
872
873 # Returns the users that should be notified
873 # Returns the users that should be notified
874 def notified_users
874 def notified_users
875 notified = []
875 notified = []
876 # Author and assignee are always notified unless they have been
876 # Author and assignee are always notified unless they have been
877 # locked or don't want to be notified
877 # locked or don't want to be notified
878 notified << author if author
878 notified << author if author
879 if assigned_to
879 if assigned_to
880 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
880 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
881 end
881 end
882 if assigned_to_was
882 if assigned_to_was
883 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
883 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
884 end
884 end
885 notified = notified.select {|u| u.active? && u.notify_about?(self)}
885 notified = notified.select {|u| u.active? && u.notify_about?(self)}
886
886
887 notified += project.notified_users
887 notified += project.notified_users
888 notified.uniq!
888 notified.uniq!
889 # Remove users that can not view the issue
889 # Remove users that can not view the issue
890 notified.reject! {|user| !visible?(user)}
890 notified.reject! {|user| !visible?(user)}
891 notified
891 notified
892 end
892 end
893
893
894 # Returns the email addresses that should be notified
894 # Returns the email addresses that should be notified
895 def recipients
895 def recipients
896 notified_users.collect(&:mail)
896 notified_users.collect(&:mail)
897 end
897 end
898
898
899 def each_notification(users, &block)
899 def each_notification(users, &block)
900 if users.any?
900 if users.any?
901 if custom_field_values.detect {|value| !value.custom_field.visible?}
901 if custom_field_values.detect {|value| !value.custom_field.visible?}
902 users_by_custom_field_visibility = users.group_by do |user|
902 users_by_custom_field_visibility = users.group_by do |user|
903 visible_custom_field_values(user).map(&:custom_field_id).sort
903 visible_custom_field_values(user).map(&:custom_field_id).sort
904 end
904 end
905 users_by_custom_field_visibility.values.each do |users|
905 users_by_custom_field_visibility.values.each do |users|
906 yield(users)
906 yield(users)
907 end
907 end
908 else
908 else
909 yield(users)
909 yield(users)
910 end
910 end
911 end
911 end
912 end
912 end
913
913
914 # Returns the number of hours spent on this issue
914 # Returns the number of hours spent on this issue
915 def spent_hours
915 def spent_hours
916 @spent_hours ||= time_entries.sum(:hours) || 0
916 @spent_hours ||= time_entries.sum(:hours) || 0
917 end
917 end
918
918
919 # Returns the total number of hours spent on this issue and its descendants
919 # Returns the total number of hours spent on this issue and its descendants
920 def total_spent_hours
920 def total_spent_hours
921 if leaf?
921 @total_spent_hours ||= if leaf?
922 spent_hours
922 spent_hours
923 else
923 else
924 @total_spent_hours ||=
925 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
924 self_and_descendants.joins(:time_entries).sum("#{TimeEntry.table_name}.hours").to_f || 0.0
926 end
925 end
927 end
926 end
928
927
929 def total_estimated_hours
928 def total_estimated_hours
930 if leaf?
929 if leaf?
931 estimated_hours
930 estimated_hours
932 else
931 else
933 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
932 @total_estimated_hours ||= self_and_descendants.sum(:estimated_hours)
934 end
933 end
935 end
934 end
936
935
937 def relations
936 def relations
938 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
937 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
939 end
938 end
940
939
941 # Preloads relations for a collection of issues
940 # Preloads relations for a collection of issues
942 def self.load_relations(issues)
941 def self.load_relations(issues)
943 if issues.any?
942 if issues.any?
944 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
943 relations = IssueRelation.where("issue_from_id IN (:ids) OR issue_to_id IN (:ids)", :ids => issues.map(&:id)).all
945 issues.each do |issue|
944 issues.each do |issue|
946 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
945 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
947 end
946 end
948 end
947 end
949 end
948 end
950
949
951 # Preloads visible spent time for a collection of issues
950 # Preloads visible total spent time for a collection of issues
952 def self.load_visible_spent_hours(issues, user=User.current)
951 def self.load_visible_spent_hours(issues, user=User.current)
953 if issues.any?
952 if issues.any?
954 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
953 hours_by_issue_id = TimeEntry.visible(user).group(:issue_id).sum(:hours)
955 issues.each do |issue|
954 issues.each do |issue|
956 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
955 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
957 end
956 end
958 end
957 end
959 end
958 end
960
959
960 def self.load_visible_total_spent_hours(issues, user=User.current)
961 if issues.any?
962 hours_by_issue_id = TimeEntry.visible(user).joins(:issue).joins("JOIN #{Issue.table_name} parent ON parent.root_id = #{Issue.table_name}.root_id" +
963 " AND parent.lft <= #{Issue.table_name}.lft AND parent.rgt >= #{Issue.table_name}.rgt").group("parent.id").sum(:hours)
964 issues.each do |issue|
965 issue.instance_variable_set "@total_spent_hours", (hours_by_issue_id[issue.id] || 0)
966 end
967 end
968 end
969
961 # Preloads visible relations for a collection of issues
970 # Preloads visible relations for a collection of issues
962 def self.load_visible_relations(issues, user=User.current)
971 def self.load_visible_relations(issues, user=User.current)
963 if issues.any?
972 if issues.any?
964 issue_ids = issues.map(&:id)
973 issue_ids = issues.map(&:id)
965 # Relations with issue_from in given issues and visible issue_to
974 # Relations with issue_from in given issues and visible issue_to
966 relations_from = IssueRelation.joins(:issue_to => :project).
975 relations_from = IssueRelation.joins(:issue_to => :project).
967 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
976 where(visible_condition(user)).where(:issue_from_id => issue_ids).to_a
968 # Relations with issue_to in given issues and visible issue_from
977 # Relations with issue_to in given issues and visible issue_from
969 relations_to = IssueRelation.joins(:issue_from => :project).
978 relations_to = IssueRelation.joins(:issue_from => :project).
970 where(visible_condition(user)).
979 where(visible_condition(user)).
971 where(:issue_to_id => issue_ids).to_a
980 where(:issue_to_id => issue_ids).to_a
972 issues.each do |issue|
981 issues.each do |issue|
973 relations =
982 relations =
974 relations_from.select {|relation| relation.issue_from_id == issue.id} +
983 relations_from.select {|relation| relation.issue_from_id == issue.id} +
975 relations_to.select {|relation| relation.issue_to_id == issue.id}
984 relations_to.select {|relation| relation.issue_to_id == issue.id}
976
985
977 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
986 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
978 end
987 end
979 end
988 end
980 end
989 end
981
990
982 # Finds an issue relation given its id.
991 # Finds an issue relation given its id.
983 def find_relation(relation_id)
992 def find_relation(relation_id)
984 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
993 IssueRelation.where("issue_to_id = ? OR issue_from_id = ?", id, id).find(relation_id)
985 end
994 end
986
995
987 # Returns all the other issues that depend on the issue
996 # Returns all the other issues that depend on the issue
988 # The algorithm is a modified breadth first search (bfs)
997 # The algorithm is a modified breadth first search (bfs)
989 def all_dependent_issues(except=[])
998 def all_dependent_issues(except=[])
990 # The found dependencies
999 # The found dependencies
991 dependencies = []
1000 dependencies = []
992
1001
993 # The visited flag for every node (issue) used by the breadth first search
1002 # The visited flag for every node (issue) used by the breadth first search
994 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
1003 eNOT_DISCOVERED = 0 # The issue is "new" to the algorithm, it has not seen it before.
995
1004
996 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
1005 ePROCESS_ALL = 1 # The issue is added to the queue. Process both children and relations of
997 # the issue when it is processed.
1006 # the issue when it is processed.
998
1007
999 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
1008 ePROCESS_RELATIONS_ONLY = 2 # The issue was added to the queue and will be output as dependent issue,
1000 # but its children will not be added to the queue when it is processed.
1009 # but its children will not be added to the queue when it is processed.
1001
1010
1002 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
1011 eRELATIONS_PROCESSED = 3 # The related issues, the parent issue and the issue itself have been added to
1003 # the queue, but its children have not been added.
1012 # the queue, but its children have not been added.
1004
1013
1005 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
1014 ePROCESS_CHILDREN_ONLY = 4 # The relations and the parent of the issue have been added to the queue, but
1006 # the children still need to be processed.
1015 # the children still need to be processed.
1007
1016
1008 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
1017 eALL_PROCESSED = 5 # The issue and all its children, its parent and its related issues have been
1009 # added as dependent issues. It needs no further processing.
1018 # added as dependent issues. It needs no further processing.
1010
1019
1011 issue_status = Hash.new(eNOT_DISCOVERED)
1020 issue_status = Hash.new(eNOT_DISCOVERED)
1012
1021
1013 # The queue
1022 # The queue
1014 queue = []
1023 queue = []
1015
1024
1016 # Initialize the bfs, add start node (self) to the queue
1025 # Initialize the bfs, add start node (self) to the queue
1017 queue << self
1026 queue << self
1018 issue_status[self] = ePROCESS_ALL
1027 issue_status[self] = ePROCESS_ALL
1019
1028
1020 while (!queue.empty?) do
1029 while (!queue.empty?) do
1021 current_issue = queue.shift
1030 current_issue = queue.shift
1022 current_issue_status = issue_status[current_issue]
1031 current_issue_status = issue_status[current_issue]
1023 dependencies << current_issue
1032 dependencies << current_issue
1024
1033
1025 # Add parent to queue, if not already in it.
1034 # Add parent to queue, if not already in it.
1026 parent = current_issue.parent
1035 parent = current_issue.parent
1027 parent_status = issue_status[parent]
1036 parent_status = issue_status[parent]
1028
1037
1029 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
1038 if parent && (parent_status == eNOT_DISCOVERED) && !except.include?(parent)
1030 queue << parent
1039 queue << parent
1031 issue_status[parent] = ePROCESS_RELATIONS_ONLY
1040 issue_status[parent] = ePROCESS_RELATIONS_ONLY
1032 end
1041 end
1033
1042
1034 # Add children to queue, but only if they are not already in it and
1043 # Add children to queue, but only if they are not already in it and
1035 # the children of the current node need to be processed.
1044 # the children of the current node need to be processed.
1036 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
1045 if (current_issue_status == ePROCESS_CHILDREN_ONLY || current_issue_status == ePROCESS_ALL)
1037 current_issue.children.each do |child|
1046 current_issue.children.each do |child|
1038 next if except.include?(child)
1047 next if except.include?(child)
1039
1048
1040 if (issue_status[child] == eNOT_DISCOVERED)
1049 if (issue_status[child] == eNOT_DISCOVERED)
1041 queue << child
1050 queue << child
1042 issue_status[child] = ePROCESS_ALL
1051 issue_status[child] = ePROCESS_ALL
1043 elsif (issue_status[child] == eRELATIONS_PROCESSED)
1052 elsif (issue_status[child] == eRELATIONS_PROCESSED)
1044 queue << child
1053 queue << child
1045 issue_status[child] = ePROCESS_CHILDREN_ONLY
1054 issue_status[child] = ePROCESS_CHILDREN_ONLY
1046 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1055 elsif (issue_status[child] == ePROCESS_RELATIONS_ONLY)
1047 queue << child
1056 queue << child
1048 issue_status[child] = ePROCESS_ALL
1057 issue_status[child] = ePROCESS_ALL
1049 end
1058 end
1050 end
1059 end
1051 end
1060 end
1052
1061
1053 # Add related issues to the queue, if they are not already in it.
1062 # Add related issues to the queue, if they are not already in it.
1054 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1063 current_issue.relations_from.map(&:issue_to).each do |related_issue|
1055 next if except.include?(related_issue)
1064 next if except.include?(related_issue)
1056
1065
1057 if (issue_status[related_issue] == eNOT_DISCOVERED)
1066 if (issue_status[related_issue] == eNOT_DISCOVERED)
1058 queue << related_issue
1067 queue << related_issue
1059 issue_status[related_issue] = ePROCESS_ALL
1068 issue_status[related_issue] = ePROCESS_ALL
1060 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1069 elsif (issue_status[related_issue] == eRELATIONS_PROCESSED)
1061 queue << related_issue
1070 queue << related_issue
1062 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1071 issue_status[related_issue] = ePROCESS_CHILDREN_ONLY
1063 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1072 elsif (issue_status[related_issue] == ePROCESS_RELATIONS_ONLY)
1064 queue << related_issue
1073 queue << related_issue
1065 issue_status[related_issue] = ePROCESS_ALL
1074 issue_status[related_issue] = ePROCESS_ALL
1066 end
1075 end
1067 end
1076 end
1068
1077
1069 # Set new status for current issue
1078 # Set new status for current issue
1070 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1079 if (current_issue_status == ePROCESS_ALL) || (current_issue_status == ePROCESS_CHILDREN_ONLY)
1071 issue_status[current_issue] = eALL_PROCESSED
1080 issue_status[current_issue] = eALL_PROCESSED
1072 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1081 elsif (current_issue_status == ePROCESS_RELATIONS_ONLY)
1073 issue_status[current_issue] = eRELATIONS_PROCESSED
1082 issue_status[current_issue] = eRELATIONS_PROCESSED
1074 end
1083 end
1075 end # while
1084 end # while
1076
1085
1077 # Remove the issues from the "except" parameter from the result array
1086 # Remove the issues from the "except" parameter from the result array
1078 dependencies -= except
1087 dependencies -= except
1079 dependencies.delete(self)
1088 dependencies.delete(self)
1080
1089
1081 dependencies
1090 dependencies
1082 end
1091 end
1083
1092
1084 # Returns an array of issues that duplicate this one
1093 # Returns an array of issues that duplicate this one
1085 def duplicates
1094 def duplicates
1086 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1095 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
1087 end
1096 end
1088
1097
1089 # Returns the due date or the target due date if any
1098 # Returns the due date or the target due date if any
1090 # Used on gantt chart
1099 # Used on gantt chart
1091 def due_before
1100 def due_before
1092 due_date || (fixed_version ? fixed_version.effective_date : nil)
1101 due_date || (fixed_version ? fixed_version.effective_date : nil)
1093 end
1102 end
1094
1103
1095 # Returns the time scheduled for this issue.
1104 # Returns the time scheduled for this issue.
1096 #
1105 #
1097 # Example:
1106 # Example:
1098 # Start Date: 2/26/09, End Date: 3/04/09
1107 # Start Date: 2/26/09, End Date: 3/04/09
1099 # duration => 6
1108 # duration => 6
1100 def duration
1109 def duration
1101 (start_date && due_date) ? due_date - start_date : 0
1110 (start_date && due_date) ? due_date - start_date : 0
1102 end
1111 end
1103
1112
1104 # Returns the duration in working days
1113 # Returns the duration in working days
1105 def working_duration
1114 def working_duration
1106 (start_date && due_date) ? working_days(start_date, due_date) : 0
1115 (start_date && due_date) ? working_days(start_date, due_date) : 0
1107 end
1116 end
1108
1117
1109 def soonest_start(reload=false)
1118 def soonest_start(reload=false)
1110 if @soonest_start.nil? || reload
1119 if @soonest_start.nil? || reload
1111 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1120 dates = relations_to(reload).collect{|relation| relation.successor_soonest_start}
1112 p = @parent_issue || parent
1121 p = @parent_issue || parent
1113 if p && Setting.parent_issue_dates == 'derived'
1122 if p && Setting.parent_issue_dates == 'derived'
1114 dates << p.soonest_start
1123 dates << p.soonest_start
1115 end
1124 end
1116 @soonest_start = dates.compact.max
1125 @soonest_start = dates.compact.max
1117 end
1126 end
1118 @soonest_start
1127 @soonest_start
1119 end
1128 end
1120
1129
1121 # Sets start_date on the given date or the next working day
1130 # Sets start_date on the given date or the next working day
1122 # and changes due_date to keep the same working duration.
1131 # and changes due_date to keep the same working duration.
1123 def reschedule_on(date)
1132 def reschedule_on(date)
1124 wd = working_duration
1133 wd = working_duration
1125 date = next_working_date(date)
1134 date = next_working_date(date)
1126 self.start_date = date
1135 self.start_date = date
1127 self.due_date = add_working_days(date, wd)
1136 self.due_date = add_working_days(date, wd)
1128 end
1137 end
1129
1138
1130 # Reschedules the issue on the given date or the next working day and saves the record.
1139 # Reschedules the issue on the given date or the next working day and saves the record.
1131 # If the issue is a parent task, this is done by rescheduling its subtasks.
1140 # If the issue is a parent task, this is done by rescheduling its subtasks.
1132 def reschedule_on!(date)
1141 def reschedule_on!(date)
1133 return if date.nil?
1142 return if date.nil?
1134 if leaf? || !dates_derived?
1143 if leaf? || !dates_derived?
1135 if start_date.nil? || start_date != date
1144 if start_date.nil? || start_date != date
1136 if start_date && start_date > date
1145 if start_date && start_date > date
1137 # Issue can not be moved earlier than its soonest start date
1146 # Issue can not be moved earlier than its soonest start date
1138 date = [soonest_start(true), date].compact.max
1147 date = [soonest_start(true), date].compact.max
1139 end
1148 end
1140 reschedule_on(date)
1149 reschedule_on(date)
1141 begin
1150 begin
1142 save
1151 save
1143 rescue ActiveRecord::StaleObjectError
1152 rescue ActiveRecord::StaleObjectError
1144 reload
1153 reload
1145 reschedule_on(date)
1154 reschedule_on(date)
1146 save
1155 save
1147 end
1156 end
1148 end
1157 end
1149 else
1158 else
1150 leaves.each do |leaf|
1159 leaves.each do |leaf|
1151 if leaf.start_date
1160 if leaf.start_date
1152 # Only move subtask if it starts at the same date as the parent
1161 # Only move subtask if it starts at the same date as the parent
1153 # or if it starts before the given date
1162 # or if it starts before the given date
1154 if start_date == leaf.start_date || date > leaf.start_date
1163 if start_date == leaf.start_date || date > leaf.start_date
1155 leaf.reschedule_on!(date)
1164 leaf.reschedule_on!(date)
1156 end
1165 end
1157 else
1166 else
1158 leaf.reschedule_on!(date)
1167 leaf.reschedule_on!(date)
1159 end
1168 end
1160 end
1169 end
1161 end
1170 end
1162 end
1171 end
1163
1172
1164 def dates_derived?
1173 def dates_derived?
1165 !leaf? && Setting.parent_issue_dates == 'derived'
1174 !leaf? && Setting.parent_issue_dates == 'derived'
1166 end
1175 end
1167
1176
1168 def priority_derived?
1177 def priority_derived?
1169 !leaf? && Setting.parent_issue_priority == 'derived'
1178 !leaf? && Setting.parent_issue_priority == 'derived'
1170 end
1179 end
1171
1180
1172 def done_ratio_derived?
1181 def done_ratio_derived?
1173 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1182 !leaf? && Setting.parent_issue_done_ratio == 'derived'
1174 end
1183 end
1175
1184
1176 def <=>(issue)
1185 def <=>(issue)
1177 if issue.nil?
1186 if issue.nil?
1178 -1
1187 -1
1179 elsif root_id != issue.root_id
1188 elsif root_id != issue.root_id
1180 (root_id || 0) <=> (issue.root_id || 0)
1189 (root_id || 0) <=> (issue.root_id || 0)
1181 else
1190 else
1182 (lft || 0) <=> (issue.lft || 0)
1191 (lft || 0) <=> (issue.lft || 0)
1183 end
1192 end
1184 end
1193 end
1185
1194
1186 def to_s
1195 def to_s
1187 "#{tracker} ##{id}: #{subject}"
1196 "#{tracker} ##{id}: #{subject}"
1188 end
1197 end
1189
1198
1190 # Returns a string of css classes that apply to the issue
1199 # Returns a string of css classes that apply to the issue
1191 def css_classes(user=User.current)
1200 def css_classes(user=User.current)
1192 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1201 s = "issue tracker-#{tracker_id} status-#{status_id} #{priority.try(:css_classes)}"
1193 s << ' closed' if closed?
1202 s << ' closed' if closed?
1194 s << ' overdue' if overdue?
1203 s << ' overdue' if overdue?
1195 s << ' child' if child?
1204 s << ' child' if child?
1196 s << ' parent' unless leaf?
1205 s << ' parent' unless leaf?
1197 s << ' private' if is_private?
1206 s << ' private' if is_private?
1198 if user.logged?
1207 if user.logged?
1199 s << ' created-by-me' if author_id == user.id
1208 s << ' created-by-me' if author_id == user.id
1200 s << ' assigned-to-me' if assigned_to_id == user.id
1209 s << ' assigned-to-me' if assigned_to_id == user.id
1201 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1210 s << ' assigned-to-my-group' if user.groups.any? {|g| g.id == assigned_to_id}
1202 end
1211 end
1203 s
1212 s
1204 end
1213 end
1205
1214
1206 # Unassigns issues from +version+ if it's no longer shared with issue's project
1215 # Unassigns issues from +version+ if it's no longer shared with issue's project
1207 def self.update_versions_from_sharing_change(version)
1216 def self.update_versions_from_sharing_change(version)
1208 # Update issues assigned to the version
1217 # Update issues assigned to the version
1209 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1218 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
1210 end
1219 end
1211
1220
1212 # Unassigns issues from versions that are no longer shared
1221 # Unassigns issues from versions that are no longer shared
1213 # after +project+ was moved
1222 # after +project+ was moved
1214 def self.update_versions_from_hierarchy_change(project)
1223 def self.update_versions_from_hierarchy_change(project)
1215 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1224 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1216 # Update issues of the moved projects and issues assigned to a version of a moved project
1225 # Update issues of the moved projects and issues assigned to a version of a moved project
1217 Issue.update_versions(
1226 Issue.update_versions(
1218 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1227 ["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)",
1219 moved_project_ids, moved_project_ids]
1228 moved_project_ids, moved_project_ids]
1220 )
1229 )
1221 end
1230 end
1222
1231
1223 def parent_issue_id=(arg)
1232 def parent_issue_id=(arg)
1224 s = arg.to_s.strip.presence
1233 s = arg.to_s.strip.presence
1225 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1234 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1226 @invalid_parent_issue_id = nil
1235 @invalid_parent_issue_id = nil
1227 elsif s.blank?
1236 elsif s.blank?
1228 @parent_issue = nil
1237 @parent_issue = nil
1229 @invalid_parent_issue_id = nil
1238 @invalid_parent_issue_id = nil
1230 else
1239 else
1231 @parent_issue = nil
1240 @parent_issue = nil
1232 @invalid_parent_issue_id = arg
1241 @invalid_parent_issue_id = arg
1233 end
1242 end
1234 end
1243 end
1235
1244
1236 def parent_issue_id
1245 def parent_issue_id
1237 if @invalid_parent_issue_id
1246 if @invalid_parent_issue_id
1238 @invalid_parent_issue_id
1247 @invalid_parent_issue_id
1239 elsif instance_variable_defined? :@parent_issue
1248 elsif instance_variable_defined? :@parent_issue
1240 @parent_issue.nil? ? nil : @parent_issue.id
1249 @parent_issue.nil? ? nil : @parent_issue.id
1241 else
1250 else
1242 parent_id
1251 parent_id
1243 end
1252 end
1244 end
1253 end
1245
1254
1246 def set_parent_id
1255 def set_parent_id
1247 self.parent_id = parent_issue_id
1256 self.parent_id = parent_issue_id
1248 end
1257 end
1249
1258
1250 # Returns true if issue's project is a valid
1259 # Returns true if issue's project is a valid
1251 # parent issue project
1260 # parent issue project
1252 def valid_parent_project?(issue=parent)
1261 def valid_parent_project?(issue=parent)
1253 return true if issue.nil? || issue.project_id == project_id
1262 return true if issue.nil? || issue.project_id == project_id
1254
1263
1255 case Setting.cross_project_subtasks
1264 case Setting.cross_project_subtasks
1256 when 'system'
1265 when 'system'
1257 true
1266 true
1258 when 'tree'
1267 when 'tree'
1259 issue.project.root == project.root
1268 issue.project.root == project.root
1260 when 'hierarchy'
1269 when 'hierarchy'
1261 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1270 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1262 when 'descendants'
1271 when 'descendants'
1263 issue.project.is_or_is_ancestor_of?(project)
1272 issue.project.is_or_is_ancestor_of?(project)
1264 else
1273 else
1265 false
1274 false
1266 end
1275 end
1267 end
1276 end
1268
1277
1269 # Returns an issue scope based on project and scope
1278 # Returns an issue scope based on project and scope
1270 def self.cross_project_scope(project, scope=nil)
1279 def self.cross_project_scope(project, scope=nil)
1271 if project.nil?
1280 if project.nil?
1272 return Issue
1281 return Issue
1273 end
1282 end
1274 case scope
1283 case scope
1275 when 'all', 'system'
1284 when 'all', 'system'
1276 Issue
1285 Issue
1277 when 'tree'
1286 when 'tree'
1278 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1287 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1279 :lft => project.root.lft, :rgt => project.root.rgt)
1288 :lft => project.root.lft, :rgt => project.root.rgt)
1280 when 'hierarchy'
1289 when 'hierarchy'
1281 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1290 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt) OR (#{Project.table_name}.lft < :lft AND #{Project.table_name}.rgt > :rgt)",
1282 :lft => project.lft, :rgt => project.rgt)
1291 :lft => project.lft, :rgt => project.rgt)
1283 when 'descendants'
1292 when 'descendants'
1284 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1293 Issue.joins(:project).where("(#{Project.table_name}.lft >= :lft AND #{Project.table_name}.rgt <= :rgt)",
1285 :lft => project.lft, :rgt => project.rgt)
1294 :lft => project.lft, :rgt => project.rgt)
1286 else
1295 else
1287 Issue.where(:project_id => project.id)
1296 Issue.where(:project_id => project.id)
1288 end
1297 end
1289 end
1298 end
1290
1299
1291 def self.by_tracker(project)
1300 def self.by_tracker(project)
1292 count_and_group_by(:project => project, :association => :tracker)
1301 count_and_group_by(:project => project, :association => :tracker)
1293 end
1302 end
1294
1303
1295 def self.by_version(project)
1304 def self.by_version(project)
1296 count_and_group_by(:project => project, :association => :fixed_version)
1305 count_and_group_by(:project => project, :association => :fixed_version)
1297 end
1306 end
1298
1307
1299 def self.by_priority(project)
1308 def self.by_priority(project)
1300 count_and_group_by(:project => project, :association => :priority)
1309 count_and_group_by(:project => project, :association => :priority)
1301 end
1310 end
1302
1311
1303 def self.by_category(project)
1312 def self.by_category(project)
1304 count_and_group_by(:project => project, :association => :category)
1313 count_and_group_by(:project => project, :association => :category)
1305 end
1314 end
1306
1315
1307 def self.by_assigned_to(project)
1316 def self.by_assigned_to(project)
1308 count_and_group_by(:project => project, :association => :assigned_to)
1317 count_and_group_by(:project => project, :association => :assigned_to)
1309 end
1318 end
1310
1319
1311 def self.by_author(project)
1320 def self.by_author(project)
1312 count_and_group_by(:project => project, :association => :author)
1321 count_and_group_by(:project => project, :association => :author)
1313 end
1322 end
1314
1323
1315 def self.by_subproject(project)
1324 def self.by_subproject(project)
1316 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1325 r = count_and_group_by(:project => project, :with_subprojects => true, :association => :project)
1317 r.reject {|r| r["project_id"] == project.id.to_s}
1326 r.reject {|r| r["project_id"] == project.id.to_s}
1318 end
1327 end
1319
1328
1320 # Query generator for selecting groups of issue counts for a project
1329 # Query generator for selecting groups of issue counts for a project
1321 # based on specific criteria
1330 # based on specific criteria
1322 #
1331 #
1323 # Options
1332 # Options
1324 # * project - Project to search in.
1333 # * project - Project to search in.
1325 # * with_subprojects - Includes subprojects issues if set to true.
1334 # * with_subprojects - Includes subprojects issues if set to true.
1326 # * association - Symbol. Association for grouping.
1335 # * association - Symbol. Association for grouping.
1327 def self.count_and_group_by(options)
1336 def self.count_and_group_by(options)
1328 assoc = reflect_on_association(options[:association])
1337 assoc = reflect_on_association(options[:association])
1329 select_field = assoc.foreign_key
1338 select_field = assoc.foreign_key
1330
1339
1331 Issue.
1340 Issue.
1332 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1341 visible(User.current, :project => options[:project], :with_subprojects => options[:with_subprojects]).
1333 joins(:status, assoc.name).
1342 joins(:status, assoc.name).
1334 group(:status_id, :is_closed, select_field).
1343 group(:status_id, :is_closed, select_field).
1335 count.
1344 count.
1336 map do |columns, total|
1345 map do |columns, total|
1337 status_id, is_closed, field_value = columns
1346 status_id, is_closed, field_value = columns
1338 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1347 is_closed = ['t', 'true', '1'].include?(is_closed.to_s)
1339 {
1348 {
1340 "status_id" => status_id.to_s,
1349 "status_id" => status_id.to_s,
1341 "closed" => is_closed,
1350 "closed" => is_closed,
1342 select_field => field_value.to_s,
1351 select_field => field_value.to_s,
1343 "total" => total.to_s
1352 "total" => total.to_s
1344 }
1353 }
1345 end
1354 end
1346 end
1355 end
1347
1356
1348 # Returns a scope of projects that user can assign the issue to
1357 # Returns a scope of projects that user can assign the issue to
1349 def allowed_target_projects(user=User.current)
1358 def allowed_target_projects(user=User.current)
1350 current_project = new_record? ? nil : project
1359 current_project = new_record? ? nil : project
1351 self.class.allowed_target_projects(user, current_project)
1360 self.class.allowed_target_projects(user, current_project)
1352 end
1361 end
1353
1362
1354 # Returns a scope of projects that user can assign issues to
1363 # Returns a scope of projects that user can assign issues to
1355 # If current_project is given, it will be included in the scope
1364 # If current_project is given, it will be included in the scope
1356 def self.allowed_target_projects(user=User.current, current_project=nil)
1365 def self.allowed_target_projects(user=User.current, current_project=nil)
1357 condition = Project.allowed_to_condition(user, :add_issues)
1366 condition = Project.allowed_to_condition(user, :add_issues)
1358 if current_project
1367 if current_project
1359 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1368 condition = ["(#{condition}) OR #{Project.table_name}.id = ?", current_project.id]
1360 end
1369 end
1361 Project.where(condition)
1370 Project.where(condition)
1362 end
1371 end
1363
1372
1364 private
1373 private
1365
1374
1366 def after_project_change
1375 def after_project_change
1367 # Update project_id on related time entries
1376 # Update project_id on related time entries
1368 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1377 TimeEntry.where({:issue_id => id}).update_all(["project_id = ?", project_id])
1369
1378
1370 # Delete issue relations
1379 # Delete issue relations
1371 unless Setting.cross_project_issue_relations?
1380 unless Setting.cross_project_issue_relations?
1372 relations_from.clear
1381 relations_from.clear
1373 relations_to.clear
1382 relations_to.clear
1374 end
1383 end
1375
1384
1376 # Move subtasks that were in the same project
1385 # Move subtasks that were in the same project
1377 children.each do |child|
1386 children.each do |child|
1378 next unless child.project_id == project_id_was
1387 next unless child.project_id == project_id_was
1379 # Change project and keep project
1388 # Change project and keep project
1380 child.send :project=, project, true
1389 child.send :project=, project, true
1381 unless child.save
1390 unless child.save
1382 raise ActiveRecord::Rollback
1391 raise ActiveRecord::Rollback
1383 end
1392 end
1384 end
1393 end
1385 end
1394 end
1386
1395
1387 # Callback for after the creation of an issue by copy
1396 # Callback for after the creation of an issue by copy
1388 # * adds a "copied to" relation with the copied issue
1397 # * adds a "copied to" relation with the copied issue
1389 # * copies subtasks from the copied issue
1398 # * copies subtasks from the copied issue
1390 def after_create_from_copy
1399 def after_create_from_copy
1391 return unless copy? && !@after_create_from_copy_handled
1400 return unless copy? && !@after_create_from_copy_handled
1392
1401
1393 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1402 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1394 if @current_journal
1403 if @current_journal
1395 @copied_from.init_journal(@current_journal.user)
1404 @copied_from.init_journal(@current_journal.user)
1396 end
1405 end
1397 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1406 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1398 unless relation.save
1407 unless relation.save
1399 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1408 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1400 end
1409 end
1401 end
1410 end
1402
1411
1403 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1412 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1404 copy_options = (@copy_options || {}).merge(:subtasks => false)
1413 copy_options = (@copy_options || {}).merge(:subtasks => false)
1405 copied_issue_ids = {@copied_from.id => self.id}
1414 copied_issue_ids = {@copied_from.id => self.id}
1406 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1415 @copied_from.reload.descendants.reorder("#{Issue.table_name}.lft").each do |child|
1407 # Do not copy self when copying an issue as a descendant of the copied issue
1416 # Do not copy self when copying an issue as a descendant of the copied issue
1408 next if child == self
1417 next if child == self
1409 # Do not copy subtasks of issues that were not copied
1418 # Do not copy subtasks of issues that were not copied
1410 next unless copied_issue_ids[child.parent_id]
1419 next unless copied_issue_ids[child.parent_id]
1411 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1420 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1412 unless child.visible?
1421 unless child.visible?
1413 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1422 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1414 next
1423 next
1415 end
1424 end
1416 copy = Issue.new.copy_from(child, copy_options)
1425 copy = Issue.new.copy_from(child, copy_options)
1417 if @current_journal
1426 if @current_journal
1418 copy.init_journal(@current_journal.user)
1427 copy.init_journal(@current_journal.user)
1419 end
1428 end
1420 copy.author = author
1429 copy.author = author
1421 copy.project = project
1430 copy.project = project
1422 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1431 copy.parent_issue_id = copied_issue_ids[child.parent_id]
1423 unless copy.save
1432 unless copy.save
1424 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1433 logger.error "Could not copy subtask ##{child.id} while copying ##{@copied_from.id} to ##{id} due to validation errors: #{copy.errors.full_messages.join(', ')}" if logger
1425 next
1434 next
1426 end
1435 end
1427 copied_issue_ids[child.id] = copy.id
1436 copied_issue_ids[child.id] = copy.id
1428 end
1437 end
1429 end
1438 end
1430 @after_create_from_copy_handled = true
1439 @after_create_from_copy_handled = true
1431 end
1440 end
1432
1441
1433 def update_nested_set_attributes
1442 def update_nested_set_attributes
1434 if parent_id_changed?
1443 if parent_id_changed?
1435 update_nested_set_attributes_on_parent_change
1444 update_nested_set_attributes_on_parent_change
1436 end
1445 end
1437 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1446 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1438 end
1447 end
1439
1448
1440 # Updates the nested set for when an existing issue is moved
1449 # Updates the nested set for when an existing issue is moved
1441 def update_nested_set_attributes_on_parent_change
1450 def update_nested_set_attributes_on_parent_change
1442 former_parent_id = parent_id_was
1451 former_parent_id = parent_id_was
1443 # delete invalid relations of all descendants
1452 # delete invalid relations of all descendants
1444 self_and_descendants.each do |issue|
1453 self_and_descendants.each do |issue|
1445 issue.relations.each do |relation|
1454 issue.relations.each do |relation|
1446 relation.destroy unless relation.valid?
1455 relation.destroy unless relation.valid?
1447 end
1456 end
1448 end
1457 end
1449 # update former parent
1458 # update former parent
1450 recalculate_attributes_for(former_parent_id) if former_parent_id
1459 recalculate_attributes_for(former_parent_id) if former_parent_id
1451 end
1460 end
1452
1461
1453 def update_parent_attributes
1462 def update_parent_attributes
1454 if parent_id
1463 if parent_id
1455 recalculate_attributes_for(parent_id)
1464 recalculate_attributes_for(parent_id)
1456 association(:parent).reset
1465 association(:parent).reset
1457 end
1466 end
1458 end
1467 end
1459
1468
1460 def recalculate_attributes_for(issue_id)
1469 def recalculate_attributes_for(issue_id)
1461 if issue_id && p = Issue.find_by_id(issue_id)
1470 if issue_id && p = Issue.find_by_id(issue_id)
1462 if p.priority_derived?
1471 if p.priority_derived?
1463 # priority = highest priority of children
1472 # priority = highest priority of children
1464 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1473 if priority_position = p.children.joins(:priority).maximum("#{IssuePriority.table_name}.position")
1465 p.priority = IssuePriority.find_by_position(priority_position)
1474 p.priority = IssuePriority.find_by_position(priority_position)
1466 end
1475 end
1467 end
1476 end
1468
1477
1469 if p.dates_derived?
1478 if p.dates_derived?
1470 # start/due dates = lowest/highest dates of children
1479 # start/due dates = lowest/highest dates of children
1471 p.start_date = p.children.minimum(:start_date)
1480 p.start_date = p.children.minimum(:start_date)
1472 p.due_date = p.children.maximum(:due_date)
1481 p.due_date = p.children.maximum(:due_date)
1473 if p.start_date && p.due_date && p.due_date < p.start_date
1482 if p.start_date && p.due_date && p.due_date < p.start_date
1474 p.start_date, p.due_date = p.due_date, p.start_date
1483 p.start_date, p.due_date = p.due_date, p.start_date
1475 end
1484 end
1476 end
1485 end
1477
1486
1478 if p.done_ratio_derived?
1487 if p.done_ratio_derived?
1479 # done ratio = weighted average ratio of leaves
1488 # done ratio = weighted average ratio of leaves
1480 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1489 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1481 leaves_count = p.leaves.count
1490 leaves_count = p.leaves.count
1482 if leaves_count > 0
1491 if leaves_count > 0
1483 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1492 average = p.leaves.where("estimated_hours > 0").average(:estimated_hours).to_f
1484 if average == 0
1493 if average == 0
1485 average = 1
1494 average = 1
1486 end
1495 end
1487 done = p.leaves.joins(:status).
1496 done = p.leaves.joins(:status).
1488 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1497 sum("COALESCE(CASE WHEN estimated_hours > 0 THEN estimated_hours ELSE NULL END, #{average}) " +
1489 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1498 "* (CASE WHEN is_closed = #{self.class.connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)").to_f
1490 progress = done / (average * leaves_count)
1499 progress = done / (average * leaves_count)
1491 p.done_ratio = progress.round
1500 p.done_ratio = progress.round
1492 end
1501 end
1493 end
1502 end
1494 end
1503 end
1495
1504
1496 # ancestors will be recursively updated
1505 # ancestors will be recursively updated
1497 p.save(:validate => false)
1506 p.save(:validate => false)
1498 end
1507 end
1499 end
1508 end
1500
1509
1501 # Update issues so their versions are not pointing to a
1510 # Update issues so their versions are not pointing to a
1502 # fixed_version that is not shared with the issue's project
1511 # fixed_version that is not shared with the issue's project
1503 def self.update_versions(conditions=nil)
1512 def self.update_versions(conditions=nil)
1504 # Only need to update issues with a fixed_version from
1513 # Only need to update issues with a fixed_version from
1505 # a different project and that is not systemwide shared
1514 # a different project and that is not systemwide shared
1506 Issue.joins(:project, :fixed_version).
1515 Issue.joins(:project, :fixed_version).
1507 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1516 where("#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1508 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1517 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1509 " AND #{Version.table_name}.sharing <> 'system'").
1518 " AND #{Version.table_name}.sharing <> 'system'").
1510 where(conditions).each do |issue|
1519 where(conditions).each do |issue|
1511 next if issue.project.nil? || issue.fixed_version.nil?
1520 next if issue.project.nil? || issue.fixed_version.nil?
1512 unless issue.project.shared_versions.include?(issue.fixed_version)
1521 unless issue.project.shared_versions.include?(issue.fixed_version)
1513 issue.init_journal(User.current)
1522 issue.init_journal(User.current)
1514 issue.fixed_version = nil
1523 issue.fixed_version = nil
1515 issue.save
1524 issue.save
1516 end
1525 end
1517 end
1526 end
1518 end
1527 end
1519
1528
1520 # Callback on file attachment
1529 # Callback on file attachment
1521 def attachment_added(attachment)
1530 def attachment_added(attachment)
1522 if current_journal && !attachment.new_record?
1531 if current_journal && !attachment.new_record?
1523 current_journal.journalize_attachment(attachment, :added)
1532 current_journal.journalize_attachment(attachment, :added)
1524 end
1533 end
1525 end
1534 end
1526
1535
1527 # Callback on attachment deletion
1536 # Callback on attachment deletion
1528 def attachment_removed(attachment)
1537 def attachment_removed(attachment)
1529 if current_journal && !attachment.new_record?
1538 if current_journal && !attachment.new_record?
1530 current_journal.journalize_attachment(attachment, :removed)
1539 current_journal.journalize_attachment(attachment, :removed)
1531 current_journal.save
1540 current_journal.save
1532 end
1541 end
1533 end
1542 end
1534
1543
1535 # Called after a relation is added
1544 # Called after a relation is added
1536 def relation_added(relation)
1545 def relation_added(relation)
1537 if current_journal
1546 if current_journal
1538 current_journal.journalize_relation(relation, :added)
1547 current_journal.journalize_relation(relation, :added)
1539 current_journal.save
1548 current_journal.save
1540 end
1549 end
1541 end
1550 end
1542
1551
1543 # Called after a relation is removed
1552 # Called after a relation is removed
1544 def relation_removed(relation)
1553 def relation_removed(relation)
1545 if current_journal
1554 if current_journal
1546 current_journal.journalize_relation(relation, :removed)
1555 current_journal.journalize_relation(relation, :removed)
1547 current_journal.save
1556 current_journal.save
1548 end
1557 end
1549 end
1558 end
1550
1559
1551 # Default assignment based on category
1560 # Default assignment based on category
1552 def default_assign
1561 def default_assign
1553 if assigned_to.nil? && category && category.assigned_to
1562 if assigned_to.nil? && category && category.assigned_to
1554 self.assigned_to = category.assigned_to
1563 self.assigned_to = category.assigned_to
1555 end
1564 end
1556 end
1565 end
1557
1566
1558 # Updates start/due dates of following issues
1567 # Updates start/due dates of following issues
1559 def reschedule_following_issues
1568 def reschedule_following_issues
1560 if start_date_changed? || due_date_changed?
1569 if start_date_changed? || due_date_changed?
1561 relations_from.each do |relation|
1570 relations_from.each do |relation|
1562 relation.set_issue_to_dates
1571 relation.set_issue_to_dates
1563 end
1572 end
1564 end
1573 end
1565 end
1574 end
1566
1575
1567 # Closes duplicates if the issue is being closed
1576 # Closes duplicates if the issue is being closed
1568 def close_duplicates
1577 def close_duplicates
1569 if closing?
1578 if closing?
1570 duplicates.each do |duplicate|
1579 duplicates.each do |duplicate|
1571 # Reload is needed in case the duplicate was updated by a previous duplicate
1580 # Reload is needed in case the duplicate was updated by a previous duplicate
1572 duplicate.reload
1581 duplicate.reload
1573 # Don't re-close it if it's already closed
1582 # Don't re-close it if it's already closed
1574 next if duplicate.closed?
1583 next if duplicate.closed?
1575 # Same user and notes
1584 # Same user and notes
1576 if @current_journal
1585 if @current_journal
1577 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1586 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1578 end
1587 end
1579 duplicate.update_attribute :status, self.status
1588 duplicate.update_attribute :status, self.status
1580 end
1589 end
1581 end
1590 end
1582 end
1591 end
1583
1592
1584 # Make sure updated_on is updated when adding a note and set updated_on now
1593 # Make sure updated_on is updated when adding a note and set updated_on now
1585 # so we can set closed_on with the same value on closing
1594 # so we can set closed_on with the same value on closing
1586 def force_updated_on_change
1595 def force_updated_on_change
1587 if @current_journal || changed?
1596 if @current_journal || changed?
1588 self.updated_on = current_time_from_proper_timezone
1597 self.updated_on = current_time_from_proper_timezone
1589 if new_record?
1598 if new_record?
1590 self.created_on = updated_on
1599 self.created_on = updated_on
1591 end
1600 end
1592 end
1601 end
1593 end
1602 end
1594
1603
1595 # Callback for setting closed_on when the issue is closed.
1604 # Callback for setting closed_on when the issue is closed.
1596 # The closed_on attribute stores the time of the last closing
1605 # The closed_on attribute stores the time of the last closing
1597 # and is preserved when the issue is reopened.
1606 # and is preserved when the issue is reopened.
1598 def update_closed_on
1607 def update_closed_on
1599 if closing?
1608 if closing?
1600 self.closed_on = updated_on
1609 self.closed_on = updated_on
1601 end
1610 end
1602 end
1611 end
1603
1612
1604 # Saves the changes in a Journal
1613 # Saves the changes in a Journal
1605 # Called after_save
1614 # Called after_save
1606 def create_journal
1615 def create_journal
1607 if current_journal
1616 if current_journal
1608 current_journal.save
1617 current_journal.save
1609 end
1618 end
1610 end
1619 end
1611
1620
1612 def send_notification
1621 def send_notification
1613 if Setting.notified_events.include?('issue_added')
1622 if Setting.notified_events.include?('issue_added')
1614 Mailer.deliver_issue_add(self)
1623 Mailer.deliver_issue_add(self)
1615 end
1624 end
1616 end
1625 end
1617
1626
1618 # Stores the previous assignee so we can still have access
1627 # Stores the previous assignee so we can still have access
1619 # to it during after_save callbacks (assigned_to_id_was is reset)
1628 # to it during after_save callbacks (assigned_to_id_was is reset)
1620 def set_assigned_to_was
1629 def set_assigned_to_was
1621 @previous_assigned_to_id = assigned_to_id_was
1630 @previous_assigned_to_id = assigned_to_id_was
1622 end
1631 end
1623
1632
1624 # Clears the previous assignee at the end of after_save callbacks
1633 # Clears the previous assignee at the end of after_save callbacks
1625 def clear_assigned_to_was
1634 def clear_assigned_to_was
1626 @assigned_to_was = nil
1635 @assigned_to_was = nil
1627 @previous_assigned_to_id = nil
1636 @previous_assigned_to_id = nil
1628 end
1637 end
1629
1638
1630 def clear_disabled_fields
1639 def clear_disabled_fields
1631 if tracker
1640 if tracker
1632 tracker.disabled_core_fields.each do |attribute|
1641 tracker.disabled_core_fields.each do |attribute|
1633 send "#{attribute}=", nil
1642 send "#{attribute}=", nil
1634 end
1643 end
1635 self.done_ratio ||= 0
1644 self.done_ratio ||= 0
1636 end
1645 end
1637 end
1646 end
1638 end
1647 end
@@ -1,537 +1,540
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2015 Jean-Philippe Lang
2 # Copyright (C) 2006-2015 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 IssueQuery < Query
18 class IssueQuery < Query
19
19
20 self.queried_class = Issue
20 self.queried_class = Issue
21
21
22 self.available_columns = [
22 self.available_columns = [
23 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
23 QueryColumn.new(:id, :sortable => "#{Issue.table_name}.id", :default_order => 'desc', :caption => '#', :frozen => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
24 QueryColumn.new(:project, :sortable => "#{Project.table_name}.name", :groupable => true),
25 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
25 QueryColumn.new(:tracker, :sortable => "#{Tracker.table_name}.position", :groupable => true),
26 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
26 QueryColumn.new(:parent, :sortable => ["#{Issue.table_name}.root_id", "#{Issue.table_name}.lft ASC"], :default_order => 'desc', :caption => :field_parent_issue),
27 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
27 QueryColumn.new(:status, :sortable => "#{IssueStatus.table_name}.position", :groupable => true),
28 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
28 QueryColumn.new(:priority, :sortable => "#{IssuePriority.table_name}.position", :default_order => 'desc', :groupable => true),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
29 QueryColumn.new(:subject, :sortable => "#{Issue.table_name}.subject"),
30 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
30 QueryColumn.new(:author, :sortable => lambda {User.fields_for_order_statement("authors")}, :groupable => true),
31 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
31 QueryColumn.new(:assigned_to, :sortable => lambda {User.fields_for_order_statement}, :groupable => true),
32 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
32 QueryColumn.new(:updated_on, :sortable => "#{Issue.table_name}.updated_on", :default_order => 'desc'),
33 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
33 QueryColumn.new(:category, :sortable => "#{IssueCategory.table_name}.name", :groupable => true),
34 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
34 QueryColumn.new(:fixed_version, :sortable => lambda {Version.fields_for_order_statement}, :groupable => true),
35 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
35 QueryColumn.new(:start_date, :sortable => "#{Issue.table_name}.start_date"),
36 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
36 QueryColumn.new(:due_date, :sortable => "#{Issue.table_name}.due_date"),
37 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
37 QueryColumn.new(:estimated_hours, :sortable => "#{Issue.table_name}.estimated_hours"),
38 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
38 QueryColumn.new(:done_ratio, :sortable => "#{Issue.table_name}.done_ratio", :groupable => true),
39 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
39 QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
40 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
40 QueryColumn.new(:closed_on, :sortable => "#{Issue.table_name}.closed_on", :default_order => 'desc'),
41 QueryColumn.new(:relations, :caption => :label_related_issues),
41 QueryColumn.new(:relations, :caption => :label_related_issues),
42 QueryColumn.new(:description, :inline => false)
42 QueryColumn.new(:description, :inline => false)
43 ]
43 ]
44
44
45 scope :visible, lambda {|*args|
45 scope :visible, lambda {|*args|
46 user = args.shift || User.current
46 user = args.shift || User.current
47 base = Project.allowed_to_condition(user, :view_issues, *args)
47 base = Project.allowed_to_condition(user, :view_issues, *args)
48 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
48 scope = joins("LEFT OUTER JOIN #{Project.table_name} ON #{table_name}.project_id = #{Project.table_name}.id").
49 where("#{table_name}.project_id IS NULL OR (#{base})")
49 where("#{table_name}.project_id IS NULL OR (#{base})")
50
50
51 if user.admin?
51 if user.admin?
52 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
52 scope.where("#{table_name}.visibility <> ? OR #{table_name}.user_id = ?", VISIBILITY_PRIVATE, user.id)
53 elsif user.memberships.any?
53 elsif user.memberships.any?
54 scope.where("#{table_name}.visibility = ?" +
54 scope.where("#{table_name}.visibility = ?" +
55 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
55 " OR (#{table_name}.visibility = ? AND #{table_name}.id IN (" +
56 "SELECT DISTINCT q.id FROM #{table_name} q" +
56 "SELECT DISTINCT q.id FROM #{table_name} q" +
57 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
57 " INNER JOIN #{table_name_prefix}queries_roles#{table_name_suffix} qr on qr.query_id = q.id" +
58 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
58 " INNER JOIN #{MemberRole.table_name} mr ON mr.role_id = qr.role_id" +
59 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
59 " INNER JOIN #{Member.table_name} m ON m.id = mr.member_id AND m.user_id = ?" +
60 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
60 " WHERE q.project_id IS NULL OR q.project_id = m.project_id))" +
61 " OR #{table_name}.user_id = ?",
61 " OR #{table_name}.user_id = ?",
62 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
62 VISIBILITY_PUBLIC, VISIBILITY_ROLES, user.id, user.id)
63 elsif user.logged?
63 elsif user.logged?
64 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
64 scope.where("#{table_name}.visibility = ? OR #{table_name}.user_id = ?", VISIBILITY_PUBLIC, user.id)
65 else
65 else
66 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
66 scope.where("#{table_name}.visibility = ?", VISIBILITY_PUBLIC)
67 end
67 end
68 }
68 }
69
69
70 def initialize(attributes=nil, *args)
70 def initialize(attributes=nil, *args)
71 super attributes
71 super attributes
72 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
72 self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
73 end
73 end
74
74
75 # Returns true if the query is visible to +user+ or the current user.
75 # Returns true if the query is visible to +user+ or the current user.
76 def visible?(user=User.current)
76 def visible?(user=User.current)
77 return true if user.admin?
77 return true if user.admin?
78 return false unless project.nil? || user.allowed_to?(:view_issues, project)
78 return false unless project.nil? || user.allowed_to?(:view_issues, project)
79 case visibility
79 case visibility
80 when VISIBILITY_PUBLIC
80 when VISIBILITY_PUBLIC
81 true
81 true
82 when VISIBILITY_ROLES
82 when VISIBILITY_ROLES
83 if project
83 if project
84 (user.roles_for_project(project) & roles).any?
84 (user.roles_for_project(project) & roles).any?
85 else
85 else
86 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
86 Member.where(:user_id => user.id).joins(:roles).where(:member_roles => {:role_id => roles.map(&:id)}).any?
87 end
87 end
88 else
88 else
89 user == self.user
89 user == self.user
90 end
90 end
91 end
91 end
92
92
93 def is_private?
93 def is_private?
94 visibility == VISIBILITY_PRIVATE
94 visibility == VISIBILITY_PRIVATE
95 end
95 end
96
96
97 def is_public?
97 def is_public?
98 !is_private?
98 !is_private?
99 end
99 end
100
100
101 def draw_relations
101 def draw_relations
102 r = options[:draw_relations]
102 r = options[:draw_relations]
103 r.nil? || r == '1'
103 r.nil? || r == '1'
104 end
104 end
105
105
106 def draw_relations=(arg)
106 def draw_relations=(arg)
107 options[:draw_relations] = (arg == '0' ? '0' : nil)
107 options[:draw_relations] = (arg == '0' ? '0' : nil)
108 end
108 end
109
109
110 def draw_progress_line
110 def draw_progress_line
111 r = options[:draw_progress_line]
111 r = options[:draw_progress_line]
112 r == '1'
112 r == '1'
113 end
113 end
114
114
115 def draw_progress_line=(arg)
115 def draw_progress_line=(arg)
116 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
116 options[:draw_progress_line] = (arg == '1' ? '1' : nil)
117 end
117 end
118
118
119 def build_from_params(params)
119 def build_from_params(params)
120 super
120 super
121 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
121 self.draw_relations = params[:draw_relations] || (params[:query] && params[:query][:draw_relations])
122 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
122 self.draw_progress_line = params[:draw_progress_line] || (params[:query] && params[:query][:draw_progress_line])
123 self
123 self
124 end
124 end
125
125
126 def initialize_available_filters
126 def initialize_available_filters
127 principals = []
127 principals = []
128 subprojects = []
128 subprojects = []
129 versions = []
129 versions = []
130 categories = []
130 categories = []
131 issue_custom_fields = []
131 issue_custom_fields = []
132
132
133 if project
133 if project
134 principals += project.principals.visible
134 principals += project.principals.visible
135 unless project.leaf?
135 unless project.leaf?
136 subprojects = project.descendants.visible.to_a
136 subprojects = project.descendants.visible.to_a
137 principals += Principal.member_of(subprojects).visible
137 principals += Principal.member_of(subprojects).visible
138 end
138 end
139 versions = project.shared_versions.to_a
139 versions = project.shared_versions.to_a
140 categories = project.issue_categories.to_a
140 categories = project.issue_categories.to_a
141 issue_custom_fields = project.all_issue_custom_fields
141 issue_custom_fields = project.all_issue_custom_fields
142 else
142 else
143 if all_projects.any?
143 if all_projects.any?
144 principals += Principal.member_of(all_projects).visible
144 principals += Principal.member_of(all_projects).visible
145 end
145 end
146 versions = Version.visible.where(:sharing => 'system').to_a
146 versions = Version.visible.where(:sharing => 'system').to_a
147 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
147 issue_custom_fields = IssueCustomField.where(:is_for_all => true)
148 end
148 end
149 principals.uniq!
149 principals.uniq!
150 principals.sort!
150 principals.sort!
151 principals.reject! {|p| p.is_a?(GroupBuiltin)}
151 principals.reject! {|p| p.is_a?(GroupBuiltin)}
152 users = principals.select {|p| p.is_a?(User)}
152 users = principals.select {|p| p.is_a?(User)}
153
153
154 add_available_filter "status_id",
154 add_available_filter "status_id",
155 :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
155 :type => :list_status, :values => IssueStatus.sorted.collect{|s| [s.name, s.id.to_s] }
156
156
157 if project.nil?
157 if project.nil?
158 project_values = []
158 project_values = []
159 if User.current.logged? && User.current.memberships.any?
159 if User.current.logged? && User.current.memberships.any?
160 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
160 project_values << ["<< #{l(:label_my_projects).downcase} >>", "mine"]
161 end
161 end
162 project_values += all_projects_values
162 project_values += all_projects_values
163 add_available_filter("project_id",
163 add_available_filter("project_id",
164 :type => :list, :values => project_values
164 :type => :list, :values => project_values
165 ) unless project_values.empty?
165 ) unless project_values.empty?
166 end
166 end
167
167
168 add_available_filter "tracker_id",
168 add_available_filter "tracker_id",
169 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
169 :type => :list, :values => trackers.collect{|s| [s.name, s.id.to_s] }
170 add_available_filter "priority_id",
170 add_available_filter "priority_id",
171 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
171 :type => :list, :values => IssuePriority.all.collect{|s| [s.name, s.id.to_s] }
172
172
173 author_values = []
173 author_values = []
174 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
174 author_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
175 author_values += users.collect{|s| [s.name, s.id.to_s] }
175 author_values += users.collect{|s| [s.name, s.id.to_s] }
176 add_available_filter("author_id",
176 add_available_filter("author_id",
177 :type => :list, :values => author_values
177 :type => :list, :values => author_values
178 ) unless author_values.empty?
178 ) unless author_values.empty?
179
179
180 assigned_to_values = []
180 assigned_to_values = []
181 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
181 assigned_to_values << ["<< #{l(:label_me)} >>", "me"] if User.current.logged?
182 assigned_to_values += (Setting.issue_group_assignment? ?
182 assigned_to_values += (Setting.issue_group_assignment? ?
183 principals : users).collect{|s| [s.name, s.id.to_s] }
183 principals : users).collect{|s| [s.name, s.id.to_s] }
184 add_available_filter("assigned_to_id",
184 add_available_filter("assigned_to_id",
185 :type => :list_optional, :values => assigned_to_values
185 :type => :list_optional, :values => assigned_to_values
186 ) unless assigned_to_values.empty?
186 ) unless assigned_to_values.empty?
187
187
188 group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] }
188 group_values = Group.givable.visible.collect {|g| [g.name, g.id.to_s] }
189 add_available_filter("member_of_group",
189 add_available_filter("member_of_group",
190 :type => :list_optional, :values => group_values
190 :type => :list_optional, :values => group_values
191 ) unless group_values.empty?
191 ) unless group_values.empty?
192
192
193 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
193 role_values = Role.givable.collect {|r| [r.name, r.id.to_s] }
194 add_available_filter("assigned_to_role",
194 add_available_filter("assigned_to_role",
195 :type => :list_optional, :values => role_values
195 :type => :list_optional, :values => role_values
196 ) unless role_values.empty?
196 ) unless role_values.empty?
197
197
198 if versions.any?
198 if versions.any?
199 add_available_filter "fixed_version_id",
199 add_available_filter "fixed_version_id",
200 :type => :list_optional,
200 :type => :list_optional,
201 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
201 :values => versions.sort.collect{|s| ["#{s.project.name} - #{s.name}", s.id.to_s] }
202 end
202 end
203
203
204 if categories.any?
204 if categories.any?
205 add_available_filter "category_id",
205 add_available_filter "category_id",
206 :type => :list_optional,
206 :type => :list_optional,
207 :values => categories.collect{|s| [s.name, s.id.to_s] }
207 :values => categories.collect{|s| [s.name, s.id.to_s] }
208 end
208 end
209
209
210 add_available_filter "subject", :type => :text
210 add_available_filter "subject", :type => :text
211 add_available_filter "created_on", :type => :date_past
211 add_available_filter "created_on", :type => :date_past
212 add_available_filter "updated_on", :type => :date_past
212 add_available_filter "updated_on", :type => :date_past
213 add_available_filter "closed_on", :type => :date_past
213 add_available_filter "closed_on", :type => :date_past
214 add_available_filter "start_date", :type => :date
214 add_available_filter "start_date", :type => :date
215 add_available_filter "due_date", :type => :date
215 add_available_filter "due_date", :type => :date
216 add_available_filter "estimated_hours", :type => :float
216 add_available_filter "estimated_hours", :type => :float
217 add_available_filter "done_ratio", :type => :integer
217 add_available_filter "done_ratio", :type => :integer
218
218
219 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
219 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
220 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
220 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
221 add_available_filter "is_private",
221 add_available_filter "is_private",
222 :type => :list,
222 :type => :list,
223 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
223 :values => [[l(:general_text_yes), "1"], [l(:general_text_no), "0"]]
224 end
224 end
225
225
226 if User.current.logged?
226 if User.current.logged?
227 add_available_filter "watcher_id",
227 add_available_filter "watcher_id",
228 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
228 :type => :list, :values => [["<< #{l(:label_me)} >>", "me"]]
229 end
229 end
230
230
231 if subprojects.any?
231 if subprojects.any?
232 add_available_filter "subproject_id",
232 add_available_filter "subproject_id",
233 :type => :list_subprojects,
233 :type => :list_subprojects,
234 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
234 :values => subprojects.collect{|s| [s.name, s.id.to_s] }
235 end
235 end
236
236
237 add_custom_fields_filters(issue_custom_fields)
237 add_custom_fields_filters(issue_custom_fields)
238
238
239 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
239 add_associations_custom_fields_filters :project, :author, :assigned_to, :fixed_version
240
240
241 IssueRelation::TYPES.each do |relation_type, options|
241 IssueRelation::TYPES.each do |relation_type, options|
242 add_available_filter relation_type, :type => :relation, :label => options[:name]
242 add_available_filter relation_type, :type => :relation, :label => options[:name]
243 end
243 end
244 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
244 add_available_filter "parent_id", :type => :tree, :label => :field_parent_issue
245 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
245 add_available_filter "child_id", :type => :tree, :label => :label_subtask_plural
246
246
247 Tracker.disabled_core_fields(trackers).each {|field|
247 Tracker.disabled_core_fields(trackers).each {|field|
248 delete_available_filter field
248 delete_available_filter field
249 }
249 }
250 end
250 end
251
251
252 def available_columns
252 def available_columns
253 return @available_columns if @available_columns
253 return @available_columns if @available_columns
254 @available_columns = self.class.available_columns.dup
254 @available_columns = self.class.available_columns.dup
255 @available_columns += (project ?
255 @available_columns += (project ?
256 project.all_issue_custom_fields :
256 project.all_issue_custom_fields :
257 IssueCustomField
257 IssueCustomField
258 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
258 ).visible.collect {|cf| QueryCustomFieldColumn.new(cf) }
259
259
260 if User.current.allowed_to?(:view_time_entries, project, :global => true)
260 if User.current.allowed_to?(:view_time_entries, project, :global => true)
261 index = nil
261 index = nil
262 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
262 @available_columns.each_with_index {|column, i| index = i if column.name == :estimated_hours}
263 index = (index ? index + 1 : -1)
263 index = (index ? index + 1 : -1)
264 # insert the column after estimated_hours or at the end
264 # insert the column after estimated_hours or at the end
265 @available_columns.insert index, QueryColumn.new(:spent_hours,
265 @available_columns.insert index, QueryColumn.new(:spent_hours,
266 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
266 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} WHERE #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id), 0)",
267 :default_order => 'desc',
267 :default_order => 'desc',
268 :caption => :label_spent_time
268 :caption => :label_spent_time
269 )
269 )
270 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
270 @available_columns.insert index+1, QueryColumn.new(:total_spent_hours,
271 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
271 :sortable => "COALESCE((SELECT SUM(hours) FROM #{TimeEntry.table_name} JOIN #{Issue.table_name} subtasks ON subtasks.id = #{TimeEntry.table_name}.issue_id" +
272 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
272 " WHERE subtasks.root_id = #{Issue.table_name}.root_id AND subtasks.lft >= #{Issue.table_name}.lft AND subtasks.rgt <= #{Issue.table_name}.rgt), 0)",
273 :default_order => 'desc',
273 :default_order => 'desc',
274 :caption => :label_total_spent_time
274 :caption => :label_total_spent_time
275 )
275 )
276 end
276 end
277
277
278 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
278 if User.current.allowed_to?(:set_issues_private, nil, :global => true) ||
279 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
279 User.current.allowed_to?(:set_own_issues_private, nil, :global => true)
280 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
280 @available_columns << QueryColumn.new(:is_private, :sortable => "#{Issue.table_name}.is_private")
281 end
281 end
282
282
283 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
283 disabled_fields = Tracker.disabled_core_fields(trackers).map {|field| field.sub(/_id$/, '')}
284 @available_columns.reject! {|column|
284 @available_columns.reject! {|column|
285 disabled_fields.include?(column.name.to_s)
285 disabled_fields.include?(column.name.to_s)
286 }
286 }
287
287
288 @available_columns
288 @available_columns
289 end
289 end
290
290
291 def default_columns_names
291 def default_columns_names
292 @default_columns_names ||= begin
292 @default_columns_names ||= begin
293 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
293 default_columns = Setting.issue_list_default_columns.map(&:to_sym)
294
294
295 project.present? ? default_columns : [:project] | default_columns
295 project.present? ? default_columns : [:project] | default_columns
296 end
296 end
297 end
297 end
298
298
299 # Returns the issue count
299 # Returns the issue count
300 def issue_count
300 def issue_count
301 Issue.visible.joins(:status, :project).where(statement).count
301 Issue.visible.joins(:status, :project).where(statement).count
302 rescue ::ActiveRecord::StatementInvalid => e
302 rescue ::ActiveRecord::StatementInvalid => e
303 raise StatementInvalid.new(e.message)
303 raise StatementInvalid.new(e.message)
304 end
304 end
305
305
306 # Returns the issue count by group or nil if query is not grouped
306 # Returns the issue count by group or nil if query is not grouped
307 def issue_count_by_group
307 def issue_count_by_group
308 r = nil
308 r = nil
309 if grouped?
309 if grouped?
310 begin
310 begin
311 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
311 # Rails3 will raise an (unexpected) RecordNotFound if there's only a nil group value
312 r = Issue.visible.
312 r = Issue.visible.
313 joins(:status, :project).
313 joins(:status, :project).
314 where(statement).
314 where(statement).
315 joins(joins_for_order_statement(group_by_statement)).
315 joins(joins_for_order_statement(group_by_statement)).
316 group(group_by_statement).
316 group(group_by_statement).
317 count
317 count
318 rescue ActiveRecord::RecordNotFound
318 rescue ActiveRecord::RecordNotFound
319 r = {nil => issue_count}
319 r = {nil => issue_count}
320 end
320 end
321 c = group_by_column
321 c = group_by_column
322 if c.is_a?(QueryCustomFieldColumn)
322 if c.is_a?(QueryCustomFieldColumn)
323 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
323 r = r.keys.inject({}) {|h, k| h[c.custom_field.cast_value(k)] = r[k]; h}
324 end
324 end
325 end
325 end
326 r
326 r
327 rescue ::ActiveRecord::StatementInvalid => e
327 rescue ::ActiveRecord::StatementInvalid => e
328 raise StatementInvalid.new(e.message)
328 raise StatementInvalid.new(e.message)
329 end
329 end
330
330
331 # Returns the issues
331 # Returns the issues
332 # Valid options are :order, :offset, :limit, :include, :conditions
332 # Valid options are :order, :offset, :limit, :include, :conditions
333 def issues(options={})
333 def issues(options={})
334 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
334 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
335
335
336 scope = Issue.visible.
336 scope = Issue.visible.
337 joins(:status, :project).
337 joins(:status, :project).
338 where(statement).
338 where(statement).
339 includes(([:status, :project] + (options[:include] || [])).uniq).
339 includes(([:status, :project] + (options[:include] || [])).uniq).
340 where(options[:conditions]).
340 where(options[:conditions]).
341 order(order_option).
341 order(order_option).
342 joins(joins_for_order_statement(order_option.join(','))).
342 joins(joins_for_order_statement(order_option.join(','))).
343 limit(options[:limit]).
343 limit(options[:limit]).
344 offset(options[:offset])
344 offset(options[:offset])
345
345
346 scope = scope.preload(:custom_values)
346 scope = scope.preload(:custom_values)
347 if has_column?(:author)
347 if has_column?(:author)
348 scope = scope.preload(:author)
348 scope = scope.preload(:author)
349 end
349 end
350
350
351 issues = scope.to_a
351 issues = scope.to_a
352
352
353 if has_column?(:spent_hours)
353 if has_column?(:spent_hours)
354 Issue.load_visible_spent_hours(issues)
354 Issue.load_visible_spent_hours(issues)
355 end
355 end
356 if has_column?(:total_spent_hours)
357 Issue.load_visible_total_spent_hours(issues)
358 end
356 if has_column?(:relations)
359 if has_column?(:relations)
357 Issue.load_visible_relations(issues)
360 Issue.load_visible_relations(issues)
358 end
361 end
359 issues
362 issues
360 rescue ::ActiveRecord::StatementInvalid => e
363 rescue ::ActiveRecord::StatementInvalid => e
361 raise StatementInvalid.new(e.message)
364 raise StatementInvalid.new(e.message)
362 end
365 end
363
366
364 # Returns the issues ids
367 # Returns the issues ids
365 def issue_ids(options={})
368 def issue_ids(options={})
366 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
369 order_option = [group_by_sort_order, options[:order]].flatten.reject(&:blank?)
367
370
368 Issue.visible.
371 Issue.visible.
369 joins(:status, :project).
372 joins(:status, :project).
370 where(statement).
373 where(statement).
371 includes(([:status, :project] + (options[:include] || [])).uniq).
374 includes(([:status, :project] + (options[:include] || [])).uniq).
372 references(([:status, :project] + (options[:include] || [])).uniq).
375 references(([:status, :project] + (options[:include] || [])).uniq).
373 where(options[:conditions]).
376 where(options[:conditions]).
374 order(order_option).
377 order(order_option).
375 joins(joins_for_order_statement(order_option.join(','))).
378 joins(joins_for_order_statement(order_option.join(','))).
376 limit(options[:limit]).
379 limit(options[:limit]).
377 offset(options[:offset]).
380 offset(options[:offset]).
378 pluck(:id)
381 pluck(:id)
379 rescue ::ActiveRecord::StatementInvalid => e
382 rescue ::ActiveRecord::StatementInvalid => e
380 raise StatementInvalid.new(e.message)
383 raise StatementInvalid.new(e.message)
381 end
384 end
382
385
383 # Returns the journals
386 # Returns the journals
384 # Valid options are :order, :offset, :limit
387 # Valid options are :order, :offset, :limit
385 def journals(options={})
388 def journals(options={})
386 Journal.visible.
389 Journal.visible.
387 joins(:issue => [:project, :status]).
390 joins(:issue => [:project, :status]).
388 where(statement).
391 where(statement).
389 order(options[:order]).
392 order(options[:order]).
390 limit(options[:limit]).
393 limit(options[:limit]).
391 offset(options[:offset]).
394 offset(options[:offset]).
392 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
395 preload(:details, :user, {:issue => [:project, :author, :tracker, :status]}).
393 to_a
396 to_a
394 rescue ::ActiveRecord::StatementInvalid => e
397 rescue ::ActiveRecord::StatementInvalid => e
395 raise StatementInvalid.new(e.message)
398 raise StatementInvalid.new(e.message)
396 end
399 end
397
400
398 # Returns the versions
401 # Returns the versions
399 # Valid options are :conditions
402 # Valid options are :conditions
400 def versions(options={})
403 def versions(options={})
401 Version.visible.
404 Version.visible.
402 where(project_statement).
405 where(project_statement).
403 where(options[:conditions]).
406 where(options[:conditions]).
404 includes(:project).
407 includes(:project).
405 references(:project).
408 references(:project).
406 to_a
409 to_a
407 rescue ::ActiveRecord::StatementInvalid => e
410 rescue ::ActiveRecord::StatementInvalid => e
408 raise StatementInvalid.new(e.message)
411 raise StatementInvalid.new(e.message)
409 end
412 end
410
413
411 def sql_for_watcher_id_field(field, operator, value)
414 def sql_for_watcher_id_field(field, operator, value)
412 db_table = Watcher.table_name
415 db_table = Watcher.table_name
413 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
416 "#{Issue.table_name}.id #{ operator == '=' ? 'IN' : 'NOT IN' } (SELECT #{db_table}.watchable_id FROM #{db_table} WHERE #{db_table}.watchable_type='Issue' AND " +
414 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
417 sql_for_field(field, '=', value, db_table, 'user_id') + ')'
415 end
418 end
416
419
417 def sql_for_member_of_group_field(field, operator, value)
420 def sql_for_member_of_group_field(field, operator, value)
418 if operator == '*' # Any group
421 if operator == '*' # Any group
419 groups = Group.givable
422 groups = Group.givable
420 operator = '=' # Override the operator since we want to find by assigned_to
423 operator = '=' # Override the operator since we want to find by assigned_to
421 elsif operator == "!*"
424 elsif operator == "!*"
422 groups = Group.givable
425 groups = Group.givable
423 operator = '!' # Override the operator since we want to find by assigned_to
426 operator = '!' # Override the operator since we want to find by assigned_to
424 else
427 else
425 groups = Group.where(:id => value).to_a
428 groups = Group.where(:id => value).to_a
426 end
429 end
427 groups ||= []
430 groups ||= []
428
431
429 members_of_groups = groups.inject([]) {|user_ids, group|
432 members_of_groups = groups.inject([]) {|user_ids, group|
430 user_ids + group.user_ids + [group.id]
433 user_ids + group.user_ids + [group.id]
431 }.uniq.compact.sort.collect(&:to_s)
434 }.uniq.compact.sort.collect(&:to_s)
432
435
433 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
436 '(' + sql_for_field("assigned_to_id", operator, members_of_groups, Issue.table_name, "assigned_to_id", false) + ')'
434 end
437 end
435
438
436 def sql_for_assigned_to_role_field(field, operator, value)
439 def sql_for_assigned_to_role_field(field, operator, value)
437 case operator
440 case operator
438 when "*", "!*" # Member / Not member
441 when "*", "!*" # Member / Not member
439 sw = operator == "!*" ? 'NOT' : ''
442 sw = operator == "!*" ? 'NOT' : ''
440 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
443 nl = operator == "!*" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
441 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
444 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}" +
442 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
445 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id))"
443 when "=", "!"
446 when "=", "!"
444 role_cond = value.any? ?
447 role_cond = value.any? ?
445 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
448 "#{MemberRole.table_name}.role_id IN (" + value.collect{|val| "'#{self.class.connection.quote_string(val)}'"}.join(",") + ")" :
446 "1=0"
449 "1=0"
447
450
448 sw = operator == "!" ? 'NOT' : ''
451 sw = operator == "!" ? 'NOT' : ''
449 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
452 nl = operator == "!" ? "#{Issue.table_name}.assigned_to_id IS NULL OR" : ''
450 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
453 "(#{nl} #{Issue.table_name}.assigned_to_id #{sw} IN (SELECT DISTINCT #{Member.table_name}.user_id FROM #{Member.table_name}, #{MemberRole.table_name}" +
451 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
454 " WHERE #{Member.table_name}.project_id = #{Issue.table_name}.project_id AND #{Member.table_name}.id = #{MemberRole.table_name}.member_id AND #{role_cond}))"
452 end
455 end
453 end
456 end
454
457
455 def sql_for_is_private_field(field, operator, value)
458 def sql_for_is_private_field(field, operator, value)
456 op = (operator == "=" ? 'IN' : 'NOT IN')
459 op = (operator == "=" ? 'IN' : 'NOT IN')
457 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
460 va = value.map {|v| v == '0' ? self.class.connection.quoted_false : self.class.connection.quoted_true}.uniq.join(',')
458
461
459 "#{Issue.table_name}.is_private #{op} (#{va})"
462 "#{Issue.table_name}.is_private #{op} (#{va})"
460 end
463 end
461
464
462 def sql_for_parent_id_field(field, operator, value)
465 def sql_for_parent_id_field(field, operator, value)
463 case operator
466 case operator
464 when "="
467 when "="
465 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
468 "#{Issue.table_name}.parent_id = #{value.first.to_i}"
466 when "~"
469 when "~"
467 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
470 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
468 if root_id && lft && rgt
471 if root_id && lft && rgt
469 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
472 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft > #{lft} AND #{Issue.table_name}.rgt < #{rgt}"
470 else
473 else
471 "1=0"
474 "1=0"
472 end
475 end
473 when "!*"
476 when "!*"
474 "#{Issue.table_name}.parent_id IS NULL"
477 "#{Issue.table_name}.parent_id IS NULL"
475 when "*"
478 when "*"
476 "#{Issue.table_name}.parent_id IS NOT NULL"
479 "#{Issue.table_name}.parent_id IS NOT NULL"
477 end
480 end
478 end
481 end
479
482
480 def sql_for_child_id_field(field, operator, value)
483 def sql_for_child_id_field(field, operator, value)
481 case operator
484 case operator
482 when "="
485 when "="
483 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
486 parent_id = Issue.where(:id => value.first.to_i).pluck(:parent_id).first
484 if parent_id
487 if parent_id
485 "#{Issue.table_name}.id = #{parent_id}"
488 "#{Issue.table_name}.id = #{parent_id}"
486 else
489 else
487 "1=0"
490 "1=0"
488 end
491 end
489 when "~"
492 when "~"
490 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
493 root_id, lft, rgt = Issue.where(:id => value.first.to_i).pluck(:root_id, :lft, :rgt).first
491 if root_id && lft && rgt
494 if root_id && lft && rgt
492 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
495 "#{Issue.table_name}.root_id = #{root_id} AND #{Issue.table_name}.lft < #{lft} AND #{Issue.table_name}.rgt > #{rgt}"
493 else
496 else
494 "1=0"
497 "1=0"
495 end
498 end
496 when "!*"
499 when "!*"
497 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
500 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft = 1"
498 when "*"
501 when "*"
499 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
502 "#{Issue.table_name}.rgt - #{Issue.table_name}.lft > 1"
500 end
503 end
501 end
504 end
502
505
503 def sql_for_relations(field, operator, value, options={})
506 def sql_for_relations(field, operator, value, options={})
504 relation_options = IssueRelation::TYPES[field]
507 relation_options = IssueRelation::TYPES[field]
505 return relation_options unless relation_options
508 return relation_options unless relation_options
506
509
507 relation_type = field
510 relation_type = field
508 join_column, target_join_column = "issue_from_id", "issue_to_id"
511 join_column, target_join_column = "issue_from_id", "issue_to_id"
509 if relation_options[:reverse] || options[:reverse]
512 if relation_options[:reverse] || options[:reverse]
510 relation_type = relation_options[:reverse] || relation_type
513 relation_type = relation_options[:reverse] || relation_type
511 join_column, target_join_column = target_join_column, join_column
514 join_column, target_join_column = target_join_column, join_column
512 end
515 end
513
516
514 sql = case operator
517 sql = case operator
515 when "*", "!*"
518 when "*", "!*"
516 op = (operator == "*" ? 'IN' : 'NOT IN')
519 op = (operator == "*" ? 'IN' : 'NOT IN')
517 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
520 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}')"
518 when "=", "!"
521 when "=", "!"
519 op = (operator == "=" ? 'IN' : 'NOT IN')
522 op = (operator == "=" ? 'IN' : 'NOT IN')
520 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
523 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name} WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = #{value.first.to_i})"
521 when "=p", "=!p", "!p"
524 when "=p", "=!p", "!p"
522 op = (operator == "!p" ? 'NOT IN' : 'IN')
525 op = (operator == "!p" ? 'NOT IN' : 'IN')
523 comp = (operator == "=!p" ? '<>' : '=')
526 comp = (operator == "=!p" ? '<>' : '=')
524 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
527 "#{Issue.table_name}.id #{op} (SELECT DISTINCT #{IssueRelation.table_name}.#{join_column} FROM #{IssueRelation.table_name}, #{Issue.table_name} relissues WHERE #{IssueRelation.table_name}.relation_type = '#{self.class.connection.quote_string(relation_type)}' AND #{IssueRelation.table_name}.#{target_join_column} = relissues.id AND relissues.project_id #{comp} #{value.first.to_i})"
525 end
528 end
526
529
527 if relation_options[:sym] == field && !options[:reverse]
530 if relation_options[:sym] == field && !options[:reverse]
528 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
531 sqls = [sql, sql_for_relations(field, operator, value, :reverse => true)]
529 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
532 sql = sqls.join(["!", "!*", "!p"].include?(operator) ? " AND " : " OR ")
530 end
533 end
531 "(#{sql})"
534 "(#{sql})"
532 end
535 end
533
536
534 IssueRelation::TYPES.keys.each do |relation_type|
537 IssueRelation::TYPES.keys.each do |relation_type|
535 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
538 alias_method "sql_for_#{relation_type}_field".to_sym, :sql_for_relations
536 end
539 end
537 end
540 end
General Comments 0
You need to be logged in to leave comments. Login now