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