##// END OF EJS Templates
Anonymous users should always see public issues only (#11872)....
Jean-Philippe Lang -
r10254:5328c4adcb6c
parent child
Show More
@@ -1,1283 +1,1283
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 belongs_to :project
21 belongs_to :project
22 belongs_to :tracker
22 belongs_to :tracker
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
23 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
24 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
25 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
26 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
27 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
28 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29
29
30 has_many :journals, :as => :journalized, :dependent => :destroy
30 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :time_entries, :dependent => :delete_all
31 has_many :time_entries, :dependent => :delete_all
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
32 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
33
33
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
34 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
35 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
36
36
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
37 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
38 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
39 acts_as_customizable
39 acts_as_customizable
40 acts_as_watchable
40 acts_as_watchable
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
41 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
42 :include => [:project, :journals],
42 :include => [:project, :journals],
43 # sort by id so that limited eager loading doesn't break with postgresql
43 # sort by id so that limited eager loading doesn't break with postgresql
44 :order_column => "#{table_name}.id"
44 :order_column => "#{table_name}.id"
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
45 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
46 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
47 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
48
48
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
49 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
50 :author_key => :author_id
50 :author_key => :author_id
51
51
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
52 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
53
53
54 attr_reader :current_journal
54 attr_reader :current_journal
55
55
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
56 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
57
57
58 validates_length_of :subject, :maximum => 255
58 validates_length_of :subject, :maximum => 255
59 validates_inclusion_of :done_ratio, :in => 0..100
59 validates_inclusion_of :done_ratio, :in => 0..100
60 validates_numericality_of :estimated_hours, :allow_nil => true
60 validates_numericality_of :estimated_hours, :allow_nil => true
61 validate :validate_issue, :validate_required_fields
61 validate :validate_issue, :validate_required_fields
62
62
63 scope :visible,
63 scope :visible,
64 lambda {|*args| { :include => :project,
64 lambda {|*args| { :include => :project,
65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
65 :conditions => Issue.visible_condition(args.shift || User.current, *args) } }
66
66
67 scope :open, lambda {|*args|
67 scope :open, lambda {|*args|
68 is_closed = args.size > 0 ? !args.first : false
68 is_closed = args.size > 0 ? !args.first : false
69 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
69 {:conditions => ["#{IssueStatus.table_name}.is_closed = ?", is_closed], :include => :status}
70 }
70 }
71
71
72 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
72 scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
73 scope :on_active_project, :include => [:status, :project, :tracker],
73 scope :on_active_project, :include => [:status, :project, :tracker],
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
74 :conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
75
75
76 before_create :default_assign
76 before_create :default_assign
77 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
77 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
78 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
79 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
80 # Should be after_create but would be called before previous after_save callbacks
80 # Should be after_create but would be called before previous after_save callbacks
81 after_save :after_create_from_copy
81 after_save :after_create_from_copy
82 after_destroy :update_parent_attributes
82 after_destroy :update_parent_attributes
83
83
84 # Returns a SQL conditions string used to find all issues visible by the specified user
84 # Returns a SQL conditions string used to find all issues visible by the specified user
85 def self.visible_condition(user, options={})
85 def self.visible_condition(user, options={})
86 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
86 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
87 case role.issues_visibility
87 if user.logged?
88 when 'all'
88 case role.issues_visibility
89 nil
89 when 'all'
90 when 'default'
90 nil
91 if user.logged?
91 when 'default'
92 user_ids = [user.id] + user.groups.map(&:id)
92 user_ids = [user.id] + user.groups.map(&:id)
93 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
93 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
94 else
94 when 'own'
95 "(#{table_name}.is_private = #{connection.quoted_false})"
96 end
97 when 'own'
98 if user.logged?
99 user_ids = [user.id] + user.groups.map(&:id)
95 user_ids = [user.id] + user.groups.map(&:id)
100 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
96 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
101 else
97 else
102 '1=0'
98 '1=0'
103 end
99 end
104 else
100 else
105 '1=0'
101 "(#{table_name}.is_private = #{connection.quoted_false})"
106 end
102 end
107 end
103 end
108 end
104 end
109
105
110 # Returns true if usr or current user is allowed to view the issue
106 # Returns true if usr or current user is allowed to view the issue
111 def visible?(usr=nil)
107 def visible?(usr=nil)
112 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
108 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
113 case role.issues_visibility
109 if user.logged?
114 when 'all'
110 case role.issues_visibility
115 true
111 when 'all'
116 when 'default'
112 true
117 !self.is_private? || (user.logged? && (self.author == user || user.is_or_belongs_to?(assigned_to)))
113 when 'default'
118 when 'own'
114 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
119 user.logged? && (self.author == user || user.is_or_belongs_to?(assigned_to))
115 when 'own'
116 self.author == user || user.is_or_belongs_to?(assigned_to)
117 else
118 false
119 end
120 else
120 else
121 false
121 !self.is_private?
122 end
122 end
123 end
123 end
124 end
124 end
125
125
126 def initialize(attributes=nil, *args)
126 def initialize(attributes=nil, *args)
127 super
127 super
128 if new_record?
128 if new_record?
129 # set default values for new records only
129 # set default values for new records only
130 self.status ||= IssueStatus.default
130 self.status ||= IssueStatus.default
131 self.priority ||= IssuePriority.default
131 self.priority ||= IssuePriority.default
132 self.watcher_user_ids = []
132 self.watcher_user_ids = []
133 end
133 end
134 end
134 end
135
135
136 # AR#Persistence#destroy would raise and RecordNotFound exception
136 # AR#Persistence#destroy would raise and RecordNotFound exception
137 # if the issue was already deleted or updated (non matching lock_version).
137 # if the issue was already deleted or updated (non matching lock_version).
138 # This is a problem when bulk deleting issues or deleting a project
138 # This is a problem when bulk deleting issues or deleting a project
139 # (because an issue may already be deleted if its parent was deleted
139 # (because an issue may already be deleted if its parent was deleted
140 # first).
140 # first).
141 # The issue is reloaded by the nested_set before being deleted so
141 # The issue is reloaded by the nested_set before being deleted so
142 # the lock_version condition should not be an issue but we handle it.
142 # the lock_version condition should not be an issue but we handle it.
143 def destroy
143 def destroy
144 super
144 super
145 rescue ActiveRecord::RecordNotFound
145 rescue ActiveRecord::RecordNotFound
146 # Stale or already deleted
146 # Stale or already deleted
147 begin
147 begin
148 reload
148 reload
149 rescue ActiveRecord::RecordNotFound
149 rescue ActiveRecord::RecordNotFound
150 # The issue was actually already deleted
150 # The issue was actually already deleted
151 @destroyed = true
151 @destroyed = true
152 return freeze
152 return freeze
153 end
153 end
154 # The issue was stale, retry to destroy
154 # The issue was stale, retry to destroy
155 super
155 super
156 end
156 end
157
157
158 def reload(*args)
158 def reload(*args)
159 @workflow_rule_by_attribute = nil
159 @workflow_rule_by_attribute = nil
160 @assignable_versions = nil
160 @assignable_versions = nil
161 super
161 super
162 end
162 end
163
163
164 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
164 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
165 def available_custom_fields
165 def available_custom_fields
166 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
166 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
167 end
167 end
168
168
169 # Copies attributes from another issue, arg can be an id or an Issue
169 # Copies attributes from another issue, arg can be an id or an Issue
170 def copy_from(arg, options={})
170 def copy_from(arg, options={})
171 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
171 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
172 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
172 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
173 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
173 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
174 self.status = issue.status
174 self.status = issue.status
175 self.author = User.current
175 self.author = User.current
176 unless options[:attachments] == false
176 unless options[:attachments] == false
177 self.attachments = issue.attachments.map do |attachement|
177 self.attachments = issue.attachments.map do |attachement|
178 attachement.copy(:container => self)
178 attachement.copy(:container => self)
179 end
179 end
180 end
180 end
181 @copied_from = issue
181 @copied_from = issue
182 @copy_options = options
182 @copy_options = options
183 self
183 self
184 end
184 end
185
185
186 # Returns an unsaved copy of the issue
186 # Returns an unsaved copy of the issue
187 def copy(attributes=nil, copy_options={})
187 def copy(attributes=nil, copy_options={})
188 copy = self.class.new.copy_from(self, copy_options)
188 copy = self.class.new.copy_from(self, copy_options)
189 copy.attributes = attributes if attributes
189 copy.attributes = attributes if attributes
190 copy
190 copy
191 end
191 end
192
192
193 # Returns true if the issue is a copy
193 # Returns true if the issue is a copy
194 def copy?
194 def copy?
195 @copied_from.present?
195 @copied_from.present?
196 end
196 end
197
197
198 # Moves/copies an issue to a new project and tracker
198 # Moves/copies an issue to a new project and tracker
199 # Returns the moved/copied issue on success, false on failure
199 # Returns the moved/copied issue on success, false on failure
200 def move_to_project(new_project, new_tracker=nil, options={})
200 def move_to_project(new_project, new_tracker=nil, options={})
201 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
201 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
202
202
203 if options[:copy]
203 if options[:copy]
204 issue = self.copy
204 issue = self.copy
205 else
205 else
206 issue = self
206 issue = self
207 end
207 end
208
208
209 issue.init_journal(User.current, options[:notes])
209 issue.init_journal(User.current, options[:notes])
210
210
211 # Preserve previous behaviour
211 # Preserve previous behaviour
212 # #move_to_project doesn't change tracker automatically
212 # #move_to_project doesn't change tracker automatically
213 issue.send :project=, new_project, true
213 issue.send :project=, new_project, true
214 if new_tracker
214 if new_tracker
215 issue.tracker = new_tracker
215 issue.tracker = new_tracker
216 end
216 end
217 # Allow bulk setting of attributes on the issue
217 # Allow bulk setting of attributes on the issue
218 if options[:attributes]
218 if options[:attributes]
219 issue.attributes = options[:attributes]
219 issue.attributes = options[:attributes]
220 end
220 end
221
221
222 issue.save ? issue : false
222 issue.save ? issue : false
223 end
223 end
224
224
225 def status_id=(sid)
225 def status_id=(sid)
226 self.status = nil
226 self.status = nil
227 result = write_attribute(:status_id, sid)
227 result = write_attribute(:status_id, sid)
228 @workflow_rule_by_attribute = nil
228 @workflow_rule_by_attribute = nil
229 result
229 result
230 end
230 end
231
231
232 def priority_id=(pid)
232 def priority_id=(pid)
233 self.priority = nil
233 self.priority = nil
234 write_attribute(:priority_id, pid)
234 write_attribute(:priority_id, pid)
235 end
235 end
236
236
237 def category_id=(cid)
237 def category_id=(cid)
238 self.category = nil
238 self.category = nil
239 write_attribute(:category_id, cid)
239 write_attribute(:category_id, cid)
240 end
240 end
241
241
242 def fixed_version_id=(vid)
242 def fixed_version_id=(vid)
243 self.fixed_version = nil
243 self.fixed_version = nil
244 write_attribute(:fixed_version_id, vid)
244 write_attribute(:fixed_version_id, vid)
245 end
245 end
246
246
247 def tracker_id=(tid)
247 def tracker_id=(tid)
248 self.tracker = nil
248 self.tracker = nil
249 result = write_attribute(:tracker_id, tid)
249 result = write_attribute(:tracker_id, tid)
250 @custom_field_values = nil
250 @custom_field_values = nil
251 @workflow_rule_by_attribute = nil
251 @workflow_rule_by_attribute = nil
252 result
252 result
253 end
253 end
254
254
255 def project_id=(project_id)
255 def project_id=(project_id)
256 if project_id.to_s != self.project_id.to_s
256 if project_id.to_s != self.project_id.to_s
257 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
257 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
258 end
258 end
259 end
259 end
260
260
261 def project=(project, keep_tracker=false)
261 def project=(project, keep_tracker=false)
262 project_was = self.project
262 project_was = self.project
263 write_attribute(:project_id, project ? project.id : nil)
263 write_attribute(:project_id, project ? project.id : nil)
264 association_instance_set('project', project)
264 association_instance_set('project', project)
265 if project_was && project && project_was != project
265 if project_was && project && project_was != project
266 @assignable_versions = nil
266 @assignable_versions = nil
267
267
268 unless keep_tracker || project.trackers.include?(tracker)
268 unless keep_tracker || project.trackers.include?(tracker)
269 self.tracker = project.trackers.first
269 self.tracker = project.trackers.first
270 end
270 end
271 # Reassign to the category with same name if any
271 # Reassign to the category with same name if any
272 if category
272 if category
273 self.category = project.issue_categories.find_by_name(category.name)
273 self.category = project.issue_categories.find_by_name(category.name)
274 end
274 end
275 # Keep the fixed_version if it's still valid in the new_project
275 # Keep the fixed_version if it's still valid in the new_project
276 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
276 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
277 self.fixed_version = nil
277 self.fixed_version = nil
278 end
278 end
279 if parent && parent.project_id != project_id
279 if parent && parent.project_id != project_id
280 self.parent_issue_id = nil
280 self.parent_issue_id = nil
281 end
281 end
282 @custom_field_values = nil
282 @custom_field_values = nil
283 end
283 end
284 end
284 end
285
285
286 def description=(arg)
286 def description=(arg)
287 if arg.is_a?(String)
287 if arg.is_a?(String)
288 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
288 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
289 end
289 end
290 write_attribute(:description, arg)
290 write_attribute(:description, arg)
291 end
291 end
292
292
293 # Overrides assign_attributes so that project and tracker get assigned first
293 # Overrides assign_attributes so that project and tracker get assigned first
294 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
294 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
295 return if new_attributes.nil?
295 return if new_attributes.nil?
296 attrs = new_attributes.dup
296 attrs = new_attributes.dup
297 attrs.stringify_keys!
297 attrs.stringify_keys!
298
298
299 %w(project project_id tracker tracker_id).each do |attr|
299 %w(project project_id tracker tracker_id).each do |attr|
300 if attrs.has_key?(attr)
300 if attrs.has_key?(attr)
301 send "#{attr}=", attrs.delete(attr)
301 send "#{attr}=", attrs.delete(attr)
302 end
302 end
303 end
303 end
304 send :assign_attributes_without_project_and_tracker_first, attrs, *args
304 send :assign_attributes_without_project_and_tracker_first, attrs, *args
305 end
305 end
306 # Do not redefine alias chain on reload (see #4838)
306 # Do not redefine alias chain on reload (see #4838)
307 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
307 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
308
308
309 def estimated_hours=(h)
309 def estimated_hours=(h)
310 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
310 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
311 end
311 end
312
312
313 safe_attributes 'project_id',
313 safe_attributes 'project_id',
314 :if => lambda {|issue, user|
314 :if => lambda {|issue, user|
315 if issue.new_record?
315 if issue.new_record?
316 issue.copy?
316 issue.copy?
317 elsif user.allowed_to?(:move_issues, issue.project)
317 elsif user.allowed_to?(:move_issues, issue.project)
318 projects = Issue.allowed_target_projects_on_move(user)
318 projects = Issue.allowed_target_projects_on_move(user)
319 projects.include?(issue.project) && projects.size > 1
319 projects.include?(issue.project) && projects.size > 1
320 end
320 end
321 }
321 }
322
322
323 safe_attributes 'tracker_id',
323 safe_attributes 'tracker_id',
324 'status_id',
324 'status_id',
325 'category_id',
325 'category_id',
326 'assigned_to_id',
326 'assigned_to_id',
327 'priority_id',
327 'priority_id',
328 'fixed_version_id',
328 'fixed_version_id',
329 'subject',
329 'subject',
330 'description',
330 'description',
331 'start_date',
331 'start_date',
332 'due_date',
332 'due_date',
333 'done_ratio',
333 'done_ratio',
334 'estimated_hours',
334 'estimated_hours',
335 'custom_field_values',
335 'custom_field_values',
336 'custom_fields',
336 'custom_fields',
337 'lock_version',
337 'lock_version',
338 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
338 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
339
339
340 safe_attributes 'status_id',
340 safe_attributes 'status_id',
341 'assigned_to_id',
341 'assigned_to_id',
342 'fixed_version_id',
342 'fixed_version_id',
343 'done_ratio',
343 'done_ratio',
344 'lock_version',
344 'lock_version',
345 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
345 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
346
346
347 safe_attributes 'watcher_user_ids',
347 safe_attributes 'watcher_user_ids',
348 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
348 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
349
349
350 safe_attributes 'is_private',
350 safe_attributes 'is_private',
351 :if => lambda {|issue, user|
351 :if => lambda {|issue, user|
352 user.allowed_to?(:set_issues_private, issue.project) ||
352 user.allowed_to?(:set_issues_private, issue.project) ||
353 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
353 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
354 }
354 }
355
355
356 safe_attributes 'parent_issue_id',
356 safe_attributes 'parent_issue_id',
357 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
357 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
358 user.allowed_to?(:manage_subtasks, issue.project)}
358 user.allowed_to?(:manage_subtasks, issue.project)}
359
359
360 def safe_attribute_names(user=nil)
360 def safe_attribute_names(user=nil)
361 names = super
361 names = super
362 names -= disabled_core_fields
362 names -= disabled_core_fields
363 names -= read_only_attribute_names(user)
363 names -= read_only_attribute_names(user)
364 names
364 names
365 end
365 end
366
366
367 # Safely sets attributes
367 # Safely sets attributes
368 # Should be called from controllers instead of #attributes=
368 # Should be called from controllers instead of #attributes=
369 # attr_accessible is too rough because we still want things like
369 # attr_accessible is too rough because we still want things like
370 # Issue.new(:project => foo) to work
370 # Issue.new(:project => foo) to work
371 def safe_attributes=(attrs, user=User.current)
371 def safe_attributes=(attrs, user=User.current)
372 return unless attrs.is_a?(Hash)
372 return unless attrs.is_a?(Hash)
373
373
374 attrs = attrs.dup
374 attrs = attrs.dup
375
375
376 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
376 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
377 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
377 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
378 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
378 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
379 self.project_id = p
379 self.project_id = p
380 end
380 end
381 end
381 end
382
382
383 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
383 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
384 self.tracker_id = t
384 self.tracker_id = t
385 end
385 end
386
386
387 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
387 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
388 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
388 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
389 self.status_id = s
389 self.status_id = s
390 end
390 end
391 end
391 end
392
392
393 attrs = delete_unsafe_attributes(attrs, user)
393 attrs = delete_unsafe_attributes(attrs, user)
394 return if attrs.empty?
394 return if attrs.empty?
395
395
396 unless leaf?
396 unless leaf?
397 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
397 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
398 end
398 end
399
399
400 if attrs['parent_issue_id'].present?
400 if attrs['parent_issue_id'].present?
401 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
401 attrs.delete('parent_issue_id') unless Issue.visible(user).exists?(attrs['parent_issue_id'].to_i)
402 end
402 end
403
403
404 if attrs['custom_field_values'].present?
404 if attrs['custom_field_values'].present?
405 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
405 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
406 end
406 end
407
407
408 if attrs['custom_fields'].present?
408 if attrs['custom_fields'].present?
409 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
409 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
410 end
410 end
411
411
412 # mass-assignment security bypass
412 # mass-assignment security bypass
413 assign_attributes attrs, :without_protection => true
413 assign_attributes attrs, :without_protection => true
414 end
414 end
415
415
416 def disabled_core_fields
416 def disabled_core_fields
417 tracker ? tracker.disabled_core_fields : []
417 tracker ? tracker.disabled_core_fields : []
418 end
418 end
419
419
420 # Returns the custom_field_values that can be edited by the given user
420 # Returns the custom_field_values that can be edited by the given user
421 def editable_custom_field_values(user=nil)
421 def editable_custom_field_values(user=nil)
422 custom_field_values.reject do |value|
422 custom_field_values.reject do |value|
423 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
423 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
424 end
424 end
425 end
425 end
426
426
427 # Returns the names of attributes that are read-only for user or the current user
427 # Returns the names of attributes that are read-only for user or the current user
428 # For users with multiple roles, the read-only fields are the intersection of
428 # For users with multiple roles, the read-only fields are the intersection of
429 # read-only fields of each role
429 # read-only fields of each role
430 # The result is an array of strings where sustom fields are represented with their ids
430 # The result is an array of strings where sustom fields are represented with their ids
431 #
431 #
432 # Examples:
432 # Examples:
433 # issue.read_only_attribute_names # => ['due_date', '2']
433 # issue.read_only_attribute_names # => ['due_date', '2']
434 # issue.read_only_attribute_names(user) # => []
434 # issue.read_only_attribute_names(user) # => []
435 def read_only_attribute_names(user=nil)
435 def read_only_attribute_names(user=nil)
436 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
436 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
437 end
437 end
438
438
439 # Returns the names of required attributes for user or the current user
439 # Returns the names of required attributes for user or the current user
440 # For users with multiple roles, the required fields are the intersection of
440 # For users with multiple roles, the required fields are the intersection of
441 # required fields of each role
441 # required fields of each role
442 # The result is an array of strings where sustom fields are represented with their ids
442 # The result is an array of strings where sustom fields are represented with their ids
443 #
443 #
444 # Examples:
444 # Examples:
445 # issue.required_attribute_names # => ['due_date', '2']
445 # issue.required_attribute_names # => ['due_date', '2']
446 # issue.required_attribute_names(user) # => []
446 # issue.required_attribute_names(user) # => []
447 def required_attribute_names(user=nil)
447 def required_attribute_names(user=nil)
448 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
448 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
449 end
449 end
450
450
451 # Returns true if the attribute is required for user
451 # Returns true if the attribute is required for user
452 def required_attribute?(name, user=nil)
452 def required_attribute?(name, user=nil)
453 required_attribute_names(user).include?(name.to_s)
453 required_attribute_names(user).include?(name.to_s)
454 end
454 end
455
455
456 # Returns a hash of the workflow rule by attribute for the given user
456 # Returns a hash of the workflow rule by attribute for the given user
457 #
457 #
458 # Examples:
458 # Examples:
459 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
459 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
460 def workflow_rule_by_attribute(user=nil)
460 def workflow_rule_by_attribute(user=nil)
461 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
461 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
462
462
463 user_real = user || User.current
463 user_real = user || User.current
464 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
464 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
465 return {} if roles.empty?
465 return {} if roles.empty?
466
466
467 result = {}
467 result = {}
468 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
468 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
469 if workflow_permissions.any?
469 if workflow_permissions.any?
470 workflow_rules = workflow_permissions.inject({}) do |h, wp|
470 workflow_rules = workflow_permissions.inject({}) do |h, wp|
471 h[wp.field_name] ||= []
471 h[wp.field_name] ||= []
472 h[wp.field_name] << wp.rule
472 h[wp.field_name] << wp.rule
473 h
473 h
474 end
474 end
475 workflow_rules.each do |attr, rules|
475 workflow_rules.each do |attr, rules|
476 next if rules.size < roles.size
476 next if rules.size < roles.size
477 uniq_rules = rules.uniq
477 uniq_rules = rules.uniq
478 if uniq_rules.size == 1
478 if uniq_rules.size == 1
479 result[attr] = uniq_rules.first
479 result[attr] = uniq_rules.first
480 else
480 else
481 result[attr] = 'required'
481 result[attr] = 'required'
482 end
482 end
483 end
483 end
484 end
484 end
485 @workflow_rule_by_attribute = result if user.nil?
485 @workflow_rule_by_attribute = result if user.nil?
486 result
486 result
487 end
487 end
488 private :workflow_rule_by_attribute
488 private :workflow_rule_by_attribute
489
489
490 def done_ratio
490 def done_ratio
491 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
491 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
492 status.default_done_ratio
492 status.default_done_ratio
493 else
493 else
494 read_attribute(:done_ratio)
494 read_attribute(:done_ratio)
495 end
495 end
496 end
496 end
497
497
498 def self.use_status_for_done_ratio?
498 def self.use_status_for_done_ratio?
499 Setting.issue_done_ratio == 'issue_status'
499 Setting.issue_done_ratio == 'issue_status'
500 end
500 end
501
501
502 def self.use_field_for_done_ratio?
502 def self.use_field_for_done_ratio?
503 Setting.issue_done_ratio == 'issue_field'
503 Setting.issue_done_ratio == 'issue_field'
504 end
504 end
505
505
506 def validate_issue
506 def validate_issue
507 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
507 if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
508 errors.add :due_date, :not_a_date
508 errors.add :due_date, :not_a_date
509 end
509 end
510
510
511 if self.due_date and self.start_date and self.due_date < self.start_date
511 if self.due_date and self.start_date and self.due_date < self.start_date
512 errors.add :due_date, :greater_than_start_date
512 errors.add :due_date, :greater_than_start_date
513 end
513 end
514
514
515 if start_date && soonest_start && start_date < soonest_start
515 if start_date && soonest_start && start_date < soonest_start
516 errors.add :start_date, :invalid
516 errors.add :start_date, :invalid
517 end
517 end
518
518
519 if fixed_version
519 if fixed_version
520 if !assignable_versions.include?(fixed_version)
520 if !assignable_versions.include?(fixed_version)
521 errors.add :fixed_version_id, :inclusion
521 errors.add :fixed_version_id, :inclusion
522 elsif reopened? && fixed_version.closed?
522 elsif reopened? && fixed_version.closed?
523 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
523 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
524 end
524 end
525 end
525 end
526
526
527 # Checks that the issue can not be added/moved to a disabled tracker
527 # Checks that the issue can not be added/moved to a disabled tracker
528 if project && (tracker_id_changed? || project_id_changed?)
528 if project && (tracker_id_changed? || project_id_changed?)
529 unless project.trackers.include?(tracker)
529 unless project.trackers.include?(tracker)
530 errors.add :tracker_id, :inclusion
530 errors.add :tracker_id, :inclusion
531 end
531 end
532 end
532 end
533
533
534 # Checks parent issue assignment
534 # Checks parent issue assignment
535 if @parent_issue
535 if @parent_issue
536 if @parent_issue.project_id != project_id
536 if @parent_issue.project_id != project_id
537 errors.add :parent_issue_id, :not_same_project
537 errors.add :parent_issue_id, :not_same_project
538 elsif !new_record?
538 elsif !new_record?
539 # moving an existing issue
539 # moving an existing issue
540 if @parent_issue.root_id != root_id
540 if @parent_issue.root_id != root_id
541 # we can always move to another tree
541 # we can always move to another tree
542 elsif move_possible?(@parent_issue)
542 elsif move_possible?(@parent_issue)
543 # move accepted inside tree
543 # move accepted inside tree
544 else
544 else
545 errors.add :parent_issue_id, :not_a_valid_parent
545 errors.add :parent_issue_id, :not_a_valid_parent
546 end
546 end
547 end
547 end
548 end
548 end
549 end
549 end
550
550
551 # Validates the issue against additional workflow requirements
551 # Validates the issue against additional workflow requirements
552 def validate_required_fields
552 def validate_required_fields
553 user = new_record? ? author : current_journal.try(:user)
553 user = new_record? ? author : current_journal.try(:user)
554
554
555 required_attribute_names(user).each do |attribute|
555 required_attribute_names(user).each do |attribute|
556 if attribute =~ /^\d+$/
556 if attribute =~ /^\d+$/
557 attribute = attribute.to_i
557 attribute = attribute.to_i
558 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
558 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
559 if v && v.value.blank?
559 if v && v.value.blank?
560 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
560 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
561 end
561 end
562 else
562 else
563 if respond_to?(attribute) && send(attribute).blank?
563 if respond_to?(attribute) && send(attribute).blank?
564 errors.add attribute, :blank
564 errors.add attribute, :blank
565 end
565 end
566 end
566 end
567 end
567 end
568 end
568 end
569
569
570 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
570 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
571 # even if the user turns off the setting later
571 # even if the user turns off the setting later
572 def update_done_ratio_from_issue_status
572 def update_done_ratio_from_issue_status
573 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
573 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
574 self.done_ratio = status.default_done_ratio
574 self.done_ratio = status.default_done_ratio
575 end
575 end
576 end
576 end
577
577
578 def init_journal(user, notes = "")
578 def init_journal(user, notes = "")
579 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
579 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
580 if new_record?
580 if new_record?
581 @current_journal.notify = false
581 @current_journal.notify = false
582 else
582 else
583 @attributes_before_change = attributes.dup
583 @attributes_before_change = attributes.dup
584 @custom_values_before_change = {}
584 @custom_values_before_change = {}
585 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
585 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
586 end
586 end
587 @current_journal
587 @current_journal
588 end
588 end
589
589
590 # Returns the id of the last journal or nil
590 # Returns the id of the last journal or nil
591 def last_journal_id
591 def last_journal_id
592 if new_record?
592 if new_record?
593 nil
593 nil
594 else
594 else
595 journals.maximum(:id)
595 journals.maximum(:id)
596 end
596 end
597 end
597 end
598
598
599 # Returns a scope for journals that have an id greater than journal_id
599 # Returns a scope for journals that have an id greater than journal_id
600 def journals_after(journal_id)
600 def journals_after(journal_id)
601 scope = journals.reorder("#{Journal.table_name}.id ASC")
601 scope = journals.reorder("#{Journal.table_name}.id ASC")
602 if journal_id.present?
602 if journal_id.present?
603 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
603 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
604 end
604 end
605 scope
605 scope
606 end
606 end
607
607
608 # Return true if the issue is closed, otherwise false
608 # Return true if the issue is closed, otherwise false
609 def closed?
609 def closed?
610 self.status.is_closed?
610 self.status.is_closed?
611 end
611 end
612
612
613 # Return true if the issue is being reopened
613 # Return true if the issue is being reopened
614 def reopened?
614 def reopened?
615 if !new_record? && status_id_changed?
615 if !new_record? && status_id_changed?
616 status_was = IssueStatus.find_by_id(status_id_was)
616 status_was = IssueStatus.find_by_id(status_id_was)
617 status_new = IssueStatus.find_by_id(status_id)
617 status_new = IssueStatus.find_by_id(status_id)
618 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
618 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
619 return true
619 return true
620 end
620 end
621 end
621 end
622 false
622 false
623 end
623 end
624
624
625 # Return true if the issue is being closed
625 # Return true if the issue is being closed
626 def closing?
626 def closing?
627 if !new_record? && status_id_changed?
627 if !new_record? && status_id_changed?
628 status_was = IssueStatus.find_by_id(status_id_was)
628 status_was = IssueStatus.find_by_id(status_id_was)
629 status_new = IssueStatus.find_by_id(status_id)
629 status_new = IssueStatus.find_by_id(status_id)
630 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
630 if status_was && status_new && !status_was.is_closed? && status_new.is_closed?
631 return true
631 return true
632 end
632 end
633 end
633 end
634 false
634 false
635 end
635 end
636
636
637 # Returns true if the issue is overdue
637 # Returns true if the issue is overdue
638 def overdue?
638 def overdue?
639 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
639 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
640 end
640 end
641
641
642 # Is the amount of work done less than it should for the due date
642 # Is the amount of work done less than it should for the due date
643 def behind_schedule?
643 def behind_schedule?
644 return false if start_date.nil? || due_date.nil?
644 return false if start_date.nil? || due_date.nil?
645 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
645 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
646 return done_date <= Date.today
646 return done_date <= Date.today
647 end
647 end
648
648
649 # Does this issue have children?
649 # Does this issue have children?
650 def children?
650 def children?
651 !leaf?
651 !leaf?
652 end
652 end
653
653
654 # Users the issue can be assigned to
654 # Users the issue can be assigned to
655 def assignable_users
655 def assignable_users
656 users = project.assignable_users
656 users = project.assignable_users
657 users << author if author
657 users << author if author
658 users << assigned_to if assigned_to
658 users << assigned_to if assigned_to
659 users.uniq.sort
659 users.uniq.sort
660 end
660 end
661
661
662 # Versions that the issue can be assigned to
662 # Versions that the issue can be assigned to
663 def assignable_versions
663 def assignable_versions
664 return @assignable_versions if @assignable_versions
664 return @assignable_versions if @assignable_versions
665
665
666 versions = project.shared_versions.open.all
666 versions = project.shared_versions.open.all
667 if fixed_version
667 if fixed_version
668 if fixed_version_id_changed?
668 if fixed_version_id_changed?
669 # nothing to do
669 # nothing to do
670 elsif project_id_changed?
670 elsif project_id_changed?
671 if project.shared_versions.include?(fixed_version)
671 if project.shared_versions.include?(fixed_version)
672 versions << fixed_version
672 versions << fixed_version
673 end
673 end
674 else
674 else
675 versions << fixed_version
675 versions << fixed_version
676 end
676 end
677 end
677 end
678 @assignable_versions = versions.uniq.sort
678 @assignable_versions = versions.uniq.sort
679 end
679 end
680
680
681 # Returns true if this issue is blocked by another issue that is still open
681 # Returns true if this issue is blocked by another issue that is still open
682 def blocked?
682 def blocked?
683 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
683 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
684 end
684 end
685
685
686 # Returns an array of statuses that user is able to apply
686 # Returns an array of statuses that user is able to apply
687 def new_statuses_allowed_to(user=User.current, include_default=false)
687 def new_statuses_allowed_to(user=User.current, include_default=false)
688 if new_record? && @copied_from
688 if new_record? && @copied_from
689 [IssueStatus.default, @copied_from.status].compact.uniq.sort
689 [IssueStatus.default, @copied_from.status].compact.uniq.sort
690 else
690 else
691 initial_status = nil
691 initial_status = nil
692 if new_record?
692 if new_record?
693 initial_status = IssueStatus.default
693 initial_status = IssueStatus.default
694 elsif status_id_was
694 elsif status_id_was
695 initial_status = IssueStatus.find_by_id(status_id_was)
695 initial_status = IssueStatus.find_by_id(status_id_was)
696 end
696 end
697 initial_status ||= status
697 initial_status ||= status
698
698
699 statuses = initial_status.find_new_statuses_allowed_to(
699 statuses = initial_status.find_new_statuses_allowed_to(
700 user.admin ? Role.all : user.roles_for_project(project),
700 user.admin ? Role.all : user.roles_for_project(project),
701 tracker,
701 tracker,
702 author == user,
702 author == user,
703 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
703 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
704 )
704 )
705 statuses << initial_status unless statuses.empty?
705 statuses << initial_status unless statuses.empty?
706 statuses << IssueStatus.default if include_default
706 statuses << IssueStatus.default if include_default
707 statuses = statuses.compact.uniq.sort
707 statuses = statuses.compact.uniq.sort
708 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
708 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
709 end
709 end
710 end
710 end
711
711
712 def assigned_to_was
712 def assigned_to_was
713 if assigned_to_id_changed? && assigned_to_id_was.present?
713 if assigned_to_id_changed? && assigned_to_id_was.present?
714 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
714 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
715 end
715 end
716 end
716 end
717
717
718 # Returns the mail adresses of users that should be notified
718 # Returns the mail adresses of users that should be notified
719 def recipients
719 def recipients
720 notified = []
720 notified = []
721 # Author and assignee are always notified unless they have been
721 # Author and assignee are always notified unless they have been
722 # locked or don't want to be notified
722 # locked or don't want to be notified
723 notified << author if author
723 notified << author if author
724 if assigned_to
724 if assigned_to
725 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
725 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
726 end
726 end
727 if assigned_to_was
727 if assigned_to_was
728 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
728 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
729 end
729 end
730 notified = notified.select {|u| u.active? && u.notify_about?(self)}
730 notified = notified.select {|u| u.active? && u.notify_about?(self)}
731
731
732 notified += project.notified_users
732 notified += project.notified_users
733 notified.uniq!
733 notified.uniq!
734 # Remove users that can not view the issue
734 # Remove users that can not view the issue
735 notified.reject! {|user| !visible?(user)}
735 notified.reject! {|user| !visible?(user)}
736 notified.collect(&:mail)
736 notified.collect(&:mail)
737 end
737 end
738
738
739 # Returns the number of hours spent on this issue
739 # Returns the number of hours spent on this issue
740 def spent_hours
740 def spent_hours
741 @spent_hours ||= time_entries.sum(:hours) || 0
741 @spent_hours ||= time_entries.sum(:hours) || 0
742 end
742 end
743
743
744 # Returns the total number of hours spent on this issue and its descendants
744 # Returns the total number of hours spent on this issue and its descendants
745 #
745 #
746 # Example:
746 # Example:
747 # spent_hours => 0.0
747 # spent_hours => 0.0
748 # spent_hours => 50.2
748 # spent_hours => 50.2
749 def total_spent_hours
749 def total_spent_hours
750 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
750 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
751 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
751 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
752 end
752 end
753
753
754 def relations
754 def relations
755 @relations ||= (relations_from + relations_to).sort
755 @relations ||= (relations_from + relations_to).sort
756 end
756 end
757
757
758 # Preloads relations for a collection of issues
758 # Preloads relations for a collection of issues
759 def self.load_relations(issues)
759 def self.load_relations(issues)
760 if issues.any?
760 if issues.any?
761 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
761 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
762 issues.each do |issue|
762 issues.each do |issue|
763 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
763 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
764 end
764 end
765 end
765 end
766 end
766 end
767
767
768 # Preloads visible spent time for a collection of issues
768 # Preloads visible spent time for a collection of issues
769 def self.load_visible_spent_hours(issues, user=User.current)
769 def self.load_visible_spent_hours(issues, user=User.current)
770 if issues.any?
770 if issues.any?
771 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
771 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
772 issues.each do |issue|
772 issues.each do |issue|
773 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
773 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
774 end
774 end
775 end
775 end
776 end
776 end
777
777
778 # Finds an issue relation given its id.
778 # Finds an issue relation given its id.
779 def find_relation(relation_id)
779 def find_relation(relation_id)
780 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
780 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
781 end
781 end
782
782
783 def all_dependent_issues(except=[])
783 def all_dependent_issues(except=[])
784 except << self
784 except << self
785 dependencies = []
785 dependencies = []
786 relations_from.each do |relation|
786 relations_from.each do |relation|
787 if relation.issue_to && !except.include?(relation.issue_to)
787 if relation.issue_to && !except.include?(relation.issue_to)
788 dependencies << relation.issue_to
788 dependencies << relation.issue_to
789 dependencies += relation.issue_to.all_dependent_issues(except)
789 dependencies += relation.issue_to.all_dependent_issues(except)
790 end
790 end
791 end
791 end
792 dependencies
792 dependencies
793 end
793 end
794
794
795 # Returns an array of issues that duplicate this one
795 # Returns an array of issues that duplicate this one
796 def duplicates
796 def duplicates
797 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
797 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
798 end
798 end
799
799
800 # Returns the due date or the target due date if any
800 # Returns the due date or the target due date if any
801 # Used on gantt chart
801 # Used on gantt chart
802 def due_before
802 def due_before
803 due_date || (fixed_version ? fixed_version.effective_date : nil)
803 due_date || (fixed_version ? fixed_version.effective_date : nil)
804 end
804 end
805
805
806 # Returns the time scheduled for this issue.
806 # Returns the time scheduled for this issue.
807 #
807 #
808 # Example:
808 # Example:
809 # Start Date: 2/26/09, End Date: 3/04/09
809 # Start Date: 2/26/09, End Date: 3/04/09
810 # duration => 6
810 # duration => 6
811 def duration
811 def duration
812 (start_date && due_date) ? due_date - start_date : 0
812 (start_date && due_date) ? due_date - start_date : 0
813 end
813 end
814
814
815 def soonest_start
815 def soonest_start
816 @soonest_start ||= (
816 @soonest_start ||= (
817 relations_to.collect{|relation| relation.successor_soonest_start} +
817 relations_to.collect{|relation| relation.successor_soonest_start} +
818 ancestors.collect(&:soonest_start)
818 ancestors.collect(&:soonest_start)
819 ).compact.max
819 ).compact.max
820 end
820 end
821
821
822 def reschedule_after(date)
822 def reschedule_after(date)
823 return if date.nil?
823 return if date.nil?
824 if leaf?
824 if leaf?
825 if start_date.nil? || start_date < date
825 if start_date.nil? || start_date < date
826 self.start_date, self.due_date = date, date + duration
826 self.start_date, self.due_date = date, date + duration
827 begin
827 begin
828 save
828 save
829 rescue ActiveRecord::StaleObjectError
829 rescue ActiveRecord::StaleObjectError
830 reload
830 reload
831 self.start_date, self.due_date = date, date + duration
831 self.start_date, self.due_date = date, date + duration
832 save
832 save
833 end
833 end
834 end
834 end
835 else
835 else
836 leaves.each do |leaf|
836 leaves.each do |leaf|
837 leaf.reschedule_after(date)
837 leaf.reschedule_after(date)
838 end
838 end
839 end
839 end
840 end
840 end
841
841
842 def <=>(issue)
842 def <=>(issue)
843 if issue.nil?
843 if issue.nil?
844 -1
844 -1
845 elsif root_id != issue.root_id
845 elsif root_id != issue.root_id
846 (root_id || 0) <=> (issue.root_id || 0)
846 (root_id || 0) <=> (issue.root_id || 0)
847 else
847 else
848 (lft || 0) <=> (issue.lft || 0)
848 (lft || 0) <=> (issue.lft || 0)
849 end
849 end
850 end
850 end
851
851
852 def to_s
852 def to_s
853 "#{tracker} ##{id}: #{subject}"
853 "#{tracker} ##{id}: #{subject}"
854 end
854 end
855
855
856 # Returns a string of css classes that apply to the issue
856 # Returns a string of css classes that apply to the issue
857 def css_classes
857 def css_classes
858 s = "issue status-#{status_id} priority-#{priority_id}"
858 s = "issue status-#{status_id} priority-#{priority_id}"
859 s << ' closed' if closed?
859 s << ' closed' if closed?
860 s << ' overdue' if overdue?
860 s << ' overdue' if overdue?
861 s << ' child' if child?
861 s << ' child' if child?
862 s << ' parent' unless leaf?
862 s << ' parent' unless leaf?
863 s << ' private' if is_private?
863 s << ' private' if is_private?
864 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
864 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
865 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
865 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
866 s
866 s
867 end
867 end
868
868
869 # Saves an issue and a time_entry from the parameters
869 # Saves an issue and a time_entry from the parameters
870 def save_issue_with_child_records(params, existing_time_entry=nil)
870 def save_issue_with_child_records(params, existing_time_entry=nil)
871 Issue.transaction do
871 Issue.transaction do
872 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
872 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
873 @time_entry = existing_time_entry || TimeEntry.new
873 @time_entry = existing_time_entry || TimeEntry.new
874 @time_entry.project = project
874 @time_entry.project = project
875 @time_entry.issue = self
875 @time_entry.issue = self
876 @time_entry.user = User.current
876 @time_entry.user = User.current
877 @time_entry.spent_on = User.current.today
877 @time_entry.spent_on = User.current.today
878 @time_entry.attributes = params[:time_entry]
878 @time_entry.attributes = params[:time_entry]
879 self.time_entries << @time_entry
879 self.time_entries << @time_entry
880 end
880 end
881
881
882 # TODO: Rename hook
882 # TODO: Rename hook
883 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
883 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
884 if save
884 if save
885 # TODO: Rename hook
885 # TODO: Rename hook
886 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
886 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
887 else
887 else
888 raise ActiveRecord::Rollback
888 raise ActiveRecord::Rollback
889 end
889 end
890 end
890 end
891 end
891 end
892
892
893 # Unassigns issues from +version+ if it's no longer shared with issue's project
893 # Unassigns issues from +version+ if it's no longer shared with issue's project
894 def self.update_versions_from_sharing_change(version)
894 def self.update_versions_from_sharing_change(version)
895 # Update issues assigned to the version
895 # Update issues assigned to the version
896 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
896 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
897 end
897 end
898
898
899 # Unassigns issues from versions that are no longer shared
899 # Unassigns issues from versions that are no longer shared
900 # after +project+ was moved
900 # after +project+ was moved
901 def self.update_versions_from_hierarchy_change(project)
901 def self.update_versions_from_hierarchy_change(project)
902 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
902 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
903 # Update issues of the moved projects and issues assigned to a version of a moved project
903 # Update issues of the moved projects and issues assigned to a version of a moved project
904 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
904 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
905 end
905 end
906
906
907 def parent_issue_id=(arg)
907 def parent_issue_id=(arg)
908 parent_issue_id = arg.blank? ? nil : arg.to_i
908 parent_issue_id = arg.blank? ? nil : arg.to_i
909 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
909 if parent_issue_id && @parent_issue = Issue.find_by_id(parent_issue_id)
910 @parent_issue.id
910 @parent_issue.id
911 else
911 else
912 @parent_issue = nil
912 @parent_issue = nil
913 nil
913 nil
914 end
914 end
915 end
915 end
916
916
917 def parent_issue_id
917 def parent_issue_id
918 if instance_variable_defined? :@parent_issue
918 if instance_variable_defined? :@parent_issue
919 @parent_issue.nil? ? nil : @parent_issue.id
919 @parent_issue.nil? ? nil : @parent_issue.id
920 else
920 else
921 parent_id
921 parent_id
922 end
922 end
923 end
923 end
924
924
925 # Extracted from the ReportsController.
925 # Extracted from the ReportsController.
926 def self.by_tracker(project)
926 def self.by_tracker(project)
927 count_and_group_by(:project => project,
927 count_and_group_by(:project => project,
928 :field => 'tracker_id',
928 :field => 'tracker_id',
929 :joins => Tracker.table_name)
929 :joins => Tracker.table_name)
930 end
930 end
931
931
932 def self.by_version(project)
932 def self.by_version(project)
933 count_and_group_by(:project => project,
933 count_and_group_by(:project => project,
934 :field => 'fixed_version_id',
934 :field => 'fixed_version_id',
935 :joins => Version.table_name)
935 :joins => Version.table_name)
936 end
936 end
937
937
938 def self.by_priority(project)
938 def self.by_priority(project)
939 count_and_group_by(:project => project,
939 count_and_group_by(:project => project,
940 :field => 'priority_id',
940 :field => 'priority_id',
941 :joins => IssuePriority.table_name)
941 :joins => IssuePriority.table_name)
942 end
942 end
943
943
944 def self.by_category(project)
944 def self.by_category(project)
945 count_and_group_by(:project => project,
945 count_and_group_by(:project => project,
946 :field => 'category_id',
946 :field => 'category_id',
947 :joins => IssueCategory.table_name)
947 :joins => IssueCategory.table_name)
948 end
948 end
949
949
950 def self.by_assigned_to(project)
950 def self.by_assigned_to(project)
951 count_and_group_by(:project => project,
951 count_and_group_by(:project => project,
952 :field => 'assigned_to_id',
952 :field => 'assigned_to_id',
953 :joins => User.table_name)
953 :joins => User.table_name)
954 end
954 end
955
955
956 def self.by_author(project)
956 def self.by_author(project)
957 count_and_group_by(:project => project,
957 count_and_group_by(:project => project,
958 :field => 'author_id',
958 :field => 'author_id',
959 :joins => User.table_name)
959 :joins => User.table_name)
960 end
960 end
961
961
962 def self.by_subproject(project)
962 def self.by_subproject(project)
963 ActiveRecord::Base.connection.select_all("select s.id as status_id,
963 ActiveRecord::Base.connection.select_all("select s.id as status_id,
964 s.is_closed as closed,
964 s.is_closed as closed,
965 #{Issue.table_name}.project_id as project_id,
965 #{Issue.table_name}.project_id as project_id,
966 count(#{Issue.table_name}.id) as total
966 count(#{Issue.table_name}.id) as total
967 from
967 from
968 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
968 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
969 where
969 where
970 #{Issue.table_name}.status_id=s.id
970 #{Issue.table_name}.status_id=s.id
971 and #{Issue.table_name}.project_id = #{Project.table_name}.id
971 and #{Issue.table_name}.project_id = #{Project.table_name}.id
972 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
972 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
973 and #{Issue.table_name}.project_id <> #{project.id}
973 and #{Issue.table_name}.project_id <> #{project.id}
974 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
974 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
975 end
975 end
976 # End ReportsController extraction
976 # End ReportsController extraction
977
977
978 # Returns an array of projects that user can assign the issue to
978 # Returns an array of projects that user can assign the issue to
979 def allowed_target_projects(user=User.current)
979 def allowed_target_projects(user=User.current)
980 if new_record?
980 if new_record?
981 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
981 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
982 else
982 else
983 self.class.allowed_target_projects_on_move(user)
983 self.class.allowed_target_projects_on_move(user)
984 end
984 end
985 end
985 end
986
986
987 # Returns an array of projects that user can move issues to
987 # Returns an array of projects that user can move issues to
988 def self.allowed_target_projects_on_move(user=User.current)
988 def self.allowed_target_projects_on_move(user=User.current)
989 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
989 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
990 end
990 end
991
991
992 private
992 private
993
993
994 def after_project_change
994 def after_project_change
995 # Update project_id on related time entries
995 # Update project_id on related time entries
996 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
996 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
997
997
998 # Delete issue relations
998 # Delete issue relations
999 unless Setting.cross_project_issue_relations?
999 unless Setting.cross_project_issue_relations?
1000 relations_from.clear
1000 relations_from.clear
1001 relations_to.clear
1001 relations_to.clear
1002 end
1002 end
1003
1003
1004 # Move subtasks
1004 # Move subtasks
1005 children.each do |child|
1005 children.each do |child|
1006 # Change project and keep project
1006 # Change project and keep project
1007 child.send :project=, project, true
1007 child.send :project=, project, true
1008 unless child.save
1008 unless child.save
1009 raise ActiveRecord::Rollback
1009 raise ActiveRecord::Rollback
1010 end
1010 end
1011 end
1011 end
1012 end
1012 end
1013
1013
1014 # Copies subtasks from the copied issue
1014 # Copies subtasks from the copied issue
1015 def after_create_from_copy
1015 def after_create_from_copy
1016 return unless copy?
1016 return unless copy?
1017
1017
1018 unless @copied_from.leaf? || @copy_options[:subtasks] == false || @subtasks_copied
1018 unless @copied_from.leaf? || @copy_options[:subtasks] == false || @subtasks_copied
1019 @copied_from.children.each do |child|
1019 @copied_from.children.each do |child|
1020 unless child.visible?
1020 unless child.visible?
1021 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1021 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1022 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1022 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1023 next
1023 next
1024 end
1024 end
1025 copy = Issue.new.copy_from(child, @copy_options)
1025 copy = Issue.new.copy_from(child, @copy_options)
1026 copy.author = author
1026 copy.author = author
1027 copy.project = project
1027 copy.project = project
1028 copy.parent_issue_id = id
1028 copy.parent_issue_id = id
1029 # Children subtasks are copied recursively
1029 # Children subtasks are copied recursively
1030 unless copy.save
1030 unless copy.save
1031 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
1031 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
1032 end
1032 end
1033 end
1033 end
1034 @subtasks_copied = true
1034 @subtasks_copied = true
1035 end
1035 end
1036 end
1036 end
1037
1037
1038 def update_nested_set_attributes
1038 def update_nested_set_attributes
1039 if root_id.nil?
1039 if root_id.nil?
1040 # issue was just created
1040 # issue was just created
1041 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1041 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1042 set_default_left_and_right
1042 set_default_left_and_right
1043 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1043 Issue.update_all("root_id = #{root_id}, lft = #{lft}, rgt = #{rgt}", ["id = ?", id])
1044 if @parent_issue
1044 if @parent_issue
1045 move_to_child_of(@parent_issue)
1045 move_to_child_of(@parent_issue)
1046 end
1046 end
1047 reload
1047 reload
1048 elsif parent_issue_id != parent_id
1048 elsif parent_issue_id != parent_id
1049 former_parent_id = parent_id
1049 former_parent_id = parent_id
1050 # moving an existing issue
1050 # moving an existing issue
1051 if @parent_issue && @parent_issue.root_id == root_id
1051 if @parent_issue && @parent_issue.root_id == root_id
1052 # inside the same tree
1052 # inside the same tree
1053 move_to_child_of(@parent_issue)
1053 move_to_child_of(@parent_issue)
1054 else
1054 else
1055 # to another tree
1055 # to another tree
1056 unless root?
1056 unless root?
1057 move_to_right_of(root)
1057 move_to_right_of(root)
1058 reload
1058 reload
1059 end
1059 end
1060 old_root_id = root_id
1060 old_root_id = root_id
1061 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1061 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1062 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1062 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1063 offset = target_maxright + 1 - lft
1063 offset = target_maxright + 1 - lft
1064 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1064 Issue.update_all("root_id = #{root_id}, lft = lft + #{offset}, rgt = rgt + #{offset}",
1065 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1065 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1066 self[left_column_name] = lft + offset
1066 self[left_column_name] = lft + offset
1067 self[right_column_name] = rgt + offset
1067 self[right_column_name] = rgt + offset
1068 if @parent_issue
1068 if @parent_issue
1069 move_to_child_of(@parent_issue)
1069 move_to_child_of(@parent_issue)
1070 end
1070 end
1071 end
1071 end
1072 reload
1072 reload
1073 # delete invalid relations of all descendants
1073 # delete invalid relations of all descendants
1074 self_and_descendants.each do |issue|
1074 self_and_descendants.each do |issue|
1075 issue.relations.each do |relation|
1075 issue.relations.each do |relation|
1076 relation.destroy unless relation.valid?
1076 relation.destroy unless relation.valid?
1077 end
1077 end
1078 end
1078 end
1079 # update former parent
1079 # update former parent
1080 recalculate_attributes_for(former_parent_id) if former_parent_id
1080 recalculate_attributes_for(former_parent_id) if former_parent_id
1081 end
1081 end
1082 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1082 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1083 end
1083 end
1084
1084
1085 def update_parent_attributes
1085 def update_parent_attributes
1086 recalculate_attributes_for(parent_id) if parent_id
1086 recalculate_attributes_for(parent_id) if parent_id
1087 end
1087 end
1088
1088
1089 def recalculate_attributes_for(issue_id)
1089 def recalculate_attributes_for(issue_id)
1090 if issue_id && p = Issue.find_by_id(issue_id)
1090 if issue_id && p = Issue.find_by_id(issue_id)
1091 # priority = highest priority of children
1091 # priority = highest priority of children
1092 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1092 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1093 p.priority = IssuePriority.find_by_position(priority_position)
1093 p.priority = IssuePriority.find_by_position(priority_position)
1094 end
1094 end
1095
1095
1096 # start/due dates = lowest/highest dates of children
1096 # start/due dates = lowest/highest dates of children
1097 p.start_date = p.children.minimum(:start_date)
1097 p.start_date = p.children.minimum(:start_date)
1098 p.due_date = p.children.maximum(:due_date)
1098 p.due_date = p.children.maximum(:due_date)
1099 if p.start_date && p.due_date && p.due_date < p.start_date
1099 if p.start_date && p.due_date && p.due_date < p.start_date
1100 p.start_date, p.due_date = p.due_date, p.start_date
1100 p.start_date, p.due_date = p.due_date, p.start_date
1101 end
1101 end
1102
1102
1103 # done ratio = weighted average ratio of leaves
1103 # done ratio = weighted average ratio of leaves
1104 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1104 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1105 leaves_count = p.leaves.count
1105 leaves_count = p.leaves.count
1106 if leaves_count > 0
1106 if leaves_count > 0
1107 average = p.leaves.average(:estimated_hours).to_f
1107 average = p.leaves.average(:estimated_hours).to_f
1108 if average == 0
1108 if average == 0
1109 average = 1
1109 average = 1
1110 end
1110 end
1111 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1111 done = p.leaves.sum("COALESCE(estimated_hours, #{average}) * (CASE WHEN is_closed = #{connection.quoted_true} THEN 100 ELSE COALESCE(done_ratio, 0) END)", :joins => :status).to_f
1112 progress = done / (average * leaves_count)
1112 progress = done / (average * leaves_count)
1113 p.done_ratio = progress.round
1113 p.done_ratio = progress.round
1114 end
1114 end
1115 end
1115 end
1116
1116
1117 # estimate = sum of leaves estimates
1117 # estimate = sum of leaves estimates
1118 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1118 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1119 p.estimated_hours = nil if p.estimated_hours == 0.0
1119 p.estimated_hours = nil if p.estimated_hours == 0.0
1120
1120
1121 # ancestors will be recursively updated
1121 # ancestors will be recursively updated
1122 p.save(:validate => false)
1122 p.save(:validate => false)
1123 end
1123 end
1124 end
1124 end
1125
1125
1126 # Update issues so their versions are not pointing to a
1126 # Update issues so their versions are not pointing to a
1127 # fixed_version that is not shared with the issue's project
1127 # fixed_version that is not shared with the issue's project
1128 def self.update_versions(conditions=nil)
1128 def self.update_versions(conditions=nil)
1129 # Only need to update issues with a fixed_version from
1129 # Only need to update issues with a fixed_version from
1130 # a different project and that is not systemwide shared
1130 # a different project and that is not systemwide shared
1131 Issue.scoped(:conditions => conditions).all(
1131 Issue.scoped(:conditions => conditions).all(
1132 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1132 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1133 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1133 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1134 " AND #{Version.table_name}.sharing <> 'system'",
1134 " AND #{Version.table_name}.sharing <> 'system'",
1135 :include => [:project, :fixed_version]
1135 :include => [:project, :fixed_version]
1136 ).each do |issue|
1136 ).each do |issue|
1137 next if issue.project.nil? || issue.fixed_version.nil?
1137 next if issue.project.nil? || issue.fixed_version.nil?
1138 unless issue.project.shared_versions.include?(issue.fixed_version)
1138 unless issue.project.shared_versions.include?(issue.fixed_version)
1139 issue.init_journal(User.current)
1139 issue.init_journal(User.current)
1140 issue.fixed_version = nil
1140 issue.fixed_version = nil
1141 issue.save
1141 issue.save
1142 end
1142 end
1143 end
1143 end
1144 end
1144 end
1145
1145
1146 # Callback on file attachment
1146 # Callback on file attachment
1147 def attachment_added(obj)
1147 def attachment_added(obj)
1148 if @current_journal && !obj.new_record?
1148 if @current_journal && !obj.new_record?
1149 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1149 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1150 end
1150 end
1151 end
1151 end
1152
1152
1153 # Callback on attachment deletion
1153 # Callback on attachment deletion
1154 def attachment_removed(obj)
1154 def attachment_removed(obj)
1155 if @current_journal && !obj.new_record?
1155 if @current_journal && !obj.new_record?
1156 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1156 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1157 @current_journal.save
1157 @current_journal.save
1158 end
1158 end
1159 end
1159 end
1160
1160
1161 # Default assignment based on category
1161 # Default assignment based on category
1162 def default_assign
1162 def default_assign
1163 if assigned_to.nil? && category && category.assigned_to
1163 if assigned_to.nil? && category && category.assigned_to
1164 self.assigned_to = category.assigned_to
1164 self.assigned_to = category.assigned_to
1165 end
1165 end
1166 end
1166 end
1167
1167
1168 # Updates start/due dates of following issues
1168 # Updates start/due dates of following issues
1169 def reschedule_following_issues
1169 def reschedule_following_issues
1170 if start_date_changed? || due_date_changed?
1170 if start_date_changed? || due_date_changed?
1171 relations_from.each do |relation|
1171 relations_from.each do |relation|
1172 relation.set_issue_to_dates
1172 relation.set_issue_to_dates
1173 end
1173 end
1174 end
1174 end
1175 end
1175 end
1176
1176
1177 # Closes duplicates if the issue is being closed
1177 # Closes duplicates if the issue is being closed
1178 def close_duplicates
1178 def close_duplicates
1179 if closing?
1179 if closing?
1180 duplicates.each do |duplicate|
1180 duplicates.each do |duplicate|
1181 # Reload is need in case the duplicate was updated by a previous duplicate
1181 # Reload is need in case the duplicate was updated by a previous duplicate
1182 duplicate.reload
1182 duplicate.reload
1183 # Don't re-close it if it's already closed
1183 # Don't re-close it if it's already closed
1184 next if duplicate.closed?
1184 next if duplicate.closed?
1185 # Same user and notes
1185 # Same user and notes
1186 if @current_journal
1186 if @current_journal
1187 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1187 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1188 end
1188 end
1189 duplicate.update_attribute :status, self.status
1189 duplicate.update_attribute :status, self.status
1190 end
1190 end
1191 end
1191 end
1192 end
1192 end
1193
1193
1194 # Make sure updated_on is updated when adding a note
1194 # Make sure updated_on is updated when adding a note
1195 def force_updated_on_change
1195 def force_updated_on_change
1196 if @current_journal
1196 if @current_journal
1197 self.updated_on = current_time_from_proper_timezone
1197 self.updated_on = current_time_from_proper_timezone
1198 end
1198 end
1199 end
1199 end
1200
1200
1201 # Saves the changes in a Journal
1201 # Saves the changes in a Journal
1202 # Called after_save
1202 # Called after_save
1203 def create_journal
1203 def create_journal
1204 if @current_journal
1204 if @current_journal
1205 # attributes changes
1205 # attributes changes
1206 if @attributes_before_change
1206 if @attributes_before_change
1207 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1207 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on)).each {|c|
1208 before = @attributes_before_change[c]
1208 before = @attributes_before_change[c]
1209 after = send(c)
1209 after = send(c)
1210 next if before == after || (before.blank? && after.blank?)
1210 next if before == after || (before.blank? && after.blank?)
1211 @current_journal.details << JournalDetail.new(:property => 'attr',
1211 @current_journal.details << JournalDetail.new(:property => 'attr',
1212 :prop_key => c,
1212 :prop_key => c,
1213 :old_value => before,
1213 :old_value => before,
1214 :value => after)
1214 :value => after)
1215 }
1215 }
1216 end
1216 end
1217 if @custom_values_before_change
1217 if @custom_values_before_change
1218 # custom fields changes
1218 # custom fields changes
1219 custom_field_values.each {|c|
1219 custom_field_values.each {|c|
1220 before = @custom_values_before_change[c.custom_field_id]
1220 before = @custom_values_before_change[c.custom_field_id]
1221 after = c.value
1221 after = c.value
1222 next if before == after || (before.blank? && after.blank?)
1222 next if before == after || (before.blank? && after.blank?)
1223
1223
1224 if before.is_a?(Array) || after.is_a?(Array)
1224 if before.is_a?(Array) || after.is_a?(Array)
1225 before = [before] unless before.is_a?(Array)
1225 before = [before] unless before.is_a?(Array)
1226 after = [after] unless after.is_a?(Array)
1226 after = [after] unless after.is_a?(Array)
1227
1227
1228 # values removed
1228 # values removed
1229 (before - after).reject(&:blank?).each do |value|
1229 (before - after).reject(&:blank?).each do |value|
1230 @current_journal.details << JournalDetail.new(:property => 'cf',
1230 @current_journal.details << JournalDetail.new(:property => 'cf',
1231 :prop_key => c.custom_field_id,
1231 :prop_key => c.custom_field_id,
1232 :old_value => value,
1232 :old_value => value,
1233 :value => nil)
1233 :value => nil)
1234 end
1234 end
1235 # values added
1235 # values added
1236 (after - before).reject(&:blank?).each do |value|
1236 (after - before).reject(&:blank?).each do |value|
1237 @current_journal.details << JournalDetail.new(:property => 'cf',
1237 @current_journal.details << JournalDetail.new(:property => 'cf',
1238 :prop_key => c.custom_field_id,
1238 :prop_key => c.custom_field_id,
1239 :old_value => nil,
1239 :old_value => nil,
1240 :value => value)
1240 :value => value)
1241 end
1241 end
1242 else
1242 else
1243 @current_journal.details << JournalDetail.new(:property => 'cf',
1243 @current_journal.details << JournalDetail.new(:property => 'cf',
1244 :prop_key => c.custom_field_id,
1244 :prop_key => c.custom_field_id,
1245 :old_value => before,
1245 :old_value => before,
1246 :value => after)
1246 :value => after)
1247 end
1247 end
1248 }
1248 }
1249 end
1249 end
1250 @current_journal.save
1250 @current_journal.save
1251 # reset current journal
1251 # reset current journal
1252 init_journal @current_journal.user, @current_journal.notes
1252 init_journal @current_journal.user, @current_journal.notes
1253 end
1253 end
1254 end
1254 end
1255
1255
1256 # Query generator for selecting groups of issue counts for a project
1256 # Query generator for selecting groups of issue counts for a project
1257 # based on specific criteria
1257 # based on specific criteria
1258 #
1258 #
1259 # Options
1259 # Options
1260 # * project - Project to search in.
1260 # * project - Project to search in.
1261 # * field - String. Issue field to key off of in the grouping.
1261 # * field - String. Issue field to key off of in the grouping.
1262 # * joins - String. The table name to join against.
1262 # * joins - String. The table name to join against.
1263 def self.count_and_group_by(options)
1263 def self.count_and_group_by(options)
1264 project = options.delete(:project)
1264 project = options.delete(:project)
1265 select_field = options.delete(:field)
1265 select_field = options.delete(:field)
1266 joins = options.delete(:joins)
1266 joins = options.delete(:joins)
1267
1267
1268 where = "#{Issue.table_name}.#{select_field}=j.id"
1268 where = "#{Issue.table_name}.#{select_field}=j.id"
1269
1269
1270 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1270 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1271 s.is_closed as closed,
1271 s.is_closed as closed,
1272 j.id as #{select_field},
1272 j.id as #{select_field},
1273 count(#{Issue.table_name}.id) as total
1273 count(#{Issue.table_name}.id) as total
1274 from
1274 from
1275 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1275 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1276 where
1276 where
1277 #{Issue.table_name}.status_id=s.id
1277 #{Issue.table_name}.status_id=s.id
1278 and #{where}
1278 and #{where}
1279 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1279 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1280 and #{visible_condition(User.current, :project => project)}
1280 and #{visible_condition(User.current, :project => project)}
1281 group by s.id, s.is_closed, j.id")
1281 group by s.id, s.is_closed, j.id")
1282 end
1282 end
1283 end
1283 end
@@ -1,203 +1,208
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 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 Role < ActiveRecord::Base
18 class Role < ActiveRecord::Base
19 # Custom coder for the permissions attribute that should be an
19 # Custom coder for the permissions attribute that should be an
20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
20 # array of symbols. Rails 3 uses Psych which can be *unbelievably*
21 # slow on some platforms (eg. mingw32).
21 # slow on some platforms (eg. mingw32).
22 class PermissionsAttributeCoder
22 class PermissionsAttributeCoder
23 def self.load(str)
23 def self.load(str)
24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
24 str.to_s.scan(/:([a-z0-9_]+)/).flatten.map(&:to_sym)
25 end
25 end
26
26
27 def self.dump(value)
27 def self.dump(value)
28 YAML.dump(value)
28 YAML.dump(value)
29 end
29 end
30 end
30 end
31
31
32 # Built-in roles
32 # Built-in roles
33 BUILTIN_NON_MEMBER = 1
33 BUILTIN_NON_MEMBER = 1
34 BUILTIN_ANONYMOUS = 2
34 BUILTIN_ANONYMOUS = 2
35
35
36 ISSUES_VISIBILITY_OPTIONS = [
36 ISSUES_VISIBILITY_OPTIONS = [
37 ['all', :label_issues_visibility_all],
37 ['all', :label_issues_visibility_all],
38 ['default', :label_issues_visibility_public],
38 ['default', :label_issues_visibility_public],
39 ['own', :label_issues_visibility_own]
39 ['own', :label_issues_visibility_own]
40 ]
40 ]
41
41
42 scope :sorted, order("#{table_name}.builtin ASC, #{table_name}.position ASC")
42 scope :sorted, order("#{table_name}.builtin ASC, #{table_name}.position ASC")
43 scope :givable, order("#{table_name}.position ASC").where(:builtin => 0)
43 scope :givable, order("#{table_name}.position ASC").where(:builtin => 0)
44 scope :builtin, lambda { |*args|
44 scope :builtin, lambda { |*args|
45 compare = (args.first == true ? 'not' : '')
45 compare = (args.first == true ? 'not' : '')
46 where("#{compare} builtin = 0")
46 where("#{compare} builtin = 0")
47 }
47 }
48
48
49 before_destroy :check_deletable
49 before_destroy :check_deletable
50 has_many :workflow_rules, :dependent => :delete_all do
50 has_many :workflow_rules, :dependent => :delete_all do
51 def copy(source_role)
51 def copy(source_role)
52 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
52 WorkflowRule.copy(nil, source_role, nil, proxy_association.owner)
53 end
53 end
54 end
54 end
55
55
56 has_many :member_roles, :dependent => :destroy
56 has_many :member_roles, :dependent => :destroy
57 has_many :members, :through => :member_roles
57 has_many :members, :through => :member_roles
58 acts_as_list
58 acts_as_list
59
59
60 serialize :permissions, ::Role::PermissionsAttributeCoder
60 serialize :permissions, ::Role::PermissionsAttributeCoder
61 attr_protected :builtin
61 attr_protected :builtin
62
62
63 validates_presence_of :name
63 validates_presence_of :name
64 validates_uniqueness_of :name
64 validates_uniqueness_of :name
65 validates_length_of :name, :maximum => 30
65 validates_length_of :name, :maximum => 30
66 validates_inclusion_of :issues_visibility,
66 validates_inclusion_of :issues_visibility,
67 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
67 :in => ISSUES_VISIBILITY_OPTIONS.collect(&:first),
68 :if => lambda {|role| role.respond_to?(:issues_visibility)}
68 :if => lambda {|role| role.respond_to?(:issues_visibility)}
69
69
70 # Copies attributes from another role, arg can be an id or a Role
70 # Copies attributes from another role, arg can be an id or a Role
71 def copy_from(arg, options={})
71 def copy_from(arg, options={})
72 return unless arg.present?
72 return unless arg.present?
73 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
73 role = arg.is_a?(Role) ? arg : Role.find_by_id(arg.to_s)
74 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
74 self.attributes = role.attributes.dup.except("id", "name", "position", "builtin", "permissions")
75 self.permissions = role.permissions.dup
75 self.permissions = role.permissions.dup
76 self
76 self
77 end
77 end
78
78
79 def permissions=(perms)
79 def permissions=(perms)
80 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
80 perms = perms.collect {|p| p.to_sym unless p.blank? }.compact.uniq if perms
81 write_attribute(:permissions, perms)
81 write_attribute(:permissions, perms)
82 end
82 end
83
83
84 def add_permission!(*perms)
84 def add_permission!(*perms)
85 self.permissions = [] unless permissions.is_a?(Array)
85 self.permissions = [] unless permissions.is_a?(Array)
86
86
87 permissions_will_change!
87 permissions_will_change!
88 perms.each do |p|
88 perms.each do |p|
89 p = p.to_sym
89 p = p.to_sym
90 permissions << p unless permissions.include?(p)
90 permissions << p unless permissions.include?(p)
91 end
91 end
92 save!
92 save!
93 end
93 end
94
94
95 def remove_permission!(*perms)
95 def remove_permission!(*perms)
96 return unless permissions.is_a?(Array)
96 return unless permissions.is_a?(Array)
97 permissions_will_change!
97 permissions_will_change!
98 perms.each { |p| permissions.delete(p.to_sym) }
98 perms.each { |p| permissions.delete(p.to_sym) }
99 save!
99 save!
100 end
100 end
101
101
102 # Returns true if the role has the given permission
102 # Returns true if the role has the given permission
103 def has_permission?(perm)
103 def has_permission?(perm)
104 !permissions.nil? && permissions.include?(perm.to_sym)
104 !permissions.nil? && permissions.include?(perm.to_sym)
105 end
105 end
106
106
107 def <=>(role)
107 def <=>(role)
108 if role
108 if role
109 if builtin == role.builtin
109 if builtin == role.builtin
110 position <=> role.position
110 position <=> role.position
111 else
111 else
112 builtin <=> role.builtin
112 builtin <=> role.builtin
113 end
113 end
114 else
114 else
115 -1
115 -1
116 end
116 end
117 end
117 end
118
118
119 def to_s
119 def to_s
120 name
120 name
121 end
121 end
122
122
123 def name
123 def name
124 case builtin
124 case builtin
125 when 1; l(:label_role_non_member, :default => read_attribute(:name))
125 when 1; l(:label_role_non_member, :default => read_attribute(:name))
126 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
126 when 2; l(:label_role_anonymous, :default => read_attribute(:name))
127 else; read_attribute(:name)
127 else; read_attribute(:name)
128 end
128 end
129 end
129 end
130
130
131 # Return true if the role is a builtin role
131 # Return true if the role is a builtin role
132 def builtin?
132 def builtin?
133 self.builtin != 0
133 self.builtin != 0
134 end
134 end
135
135
136 # Return true if the role is the anonymous role
137 def anonymous?
138 builtin == 2
139 end
140
136 # Return true if the role is a project member role
141 # Return true if the role is a project member role
137 def member?
142 def member?
138 !self.builtin?
143 !self.builtin?
139 end
144 end
140
145
141 # Return true if role is allowed to do the specified action
146 # Return true if role is allowed to do the specified action
142 # action can be:
147 # action can be:
143 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
148 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
144 # * a permission Symbol (eg. :edit_project)
149 # * a permission Symbol (eg. :edit_project)
145 def allowed_to?(action)
150 def allowed_to?(action)
146 if action.is_a? Hash
151 if action.is_a? Hash
147 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
152 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
148 else
153 else
149 allowed_permissions.include? action
154 allowed_permissions.include? action
150 end
155 end
151 end
156 end
152
157
153 # Return all the permissions that can be given to the role
158 # Return all the permissions that can be given to the role
154 def setable_permissions
159 def setable_permissions
155 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
160 setable_permissions = Redmine::AccessControl.permissions - Redmine::AccessControl.public_permissions
156 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
161 setable_permissions -= Redmine::AccessControl.members_only_permissions if self.builtin == BUILTIN_NON_MEMBER
157 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
162 setable_permissions -= Redmine::AccessControl.loggedin_only_permissions if self.builtin == BUILTIN_ANONYMOUS
158 setable_permissions
163 setable_permissions
159 end
164 end
160
165
161 # Find all the roles that can be given to a project member
166 # Find all the roles that can be given to a project member
162 def self.find_all_givable
167 def self.find_all_givable
163 Role.givable.all
168 Role.givable.all
164 end
169 end
165
170
166 # Return the builtin 'non member' role. If the role doesn't exist,
171 # Return the builtin 'non member' role. If the role doesn't exist,
167 # it will be created on the fly.
172 # it will be created on the fly.
168 def self.non_member
173 def self.non_member
169 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
174 find_or_create_system_role(BUILTIN_NON_MEMBER, 'Non member')
170 end
175 end
171
176
172 # Return the builtin 'anonymous' role. If the role doesn't exist,
177 # Return the builtin 'anonymous' role. If the role doesn't exist,
173 # it will be created on the fly.
178 # it will be created on the fly.
174 def self.anonymous
179 def self.anonymous
175 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
180 find_or_create_system_role(BUILTIN_ANONYMOUS, 'Anonymous')
176 end
181 end
177
182
178 private
183 private
179
184
180 def allowed_permissions
185 def allowed_permissions
181 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
186 @allowed_permissions ||= permissions + Redmine::AccessControl.public_permissions.collect {|p| p.name}
182 end
187 end
183
188
184 def allowed_actions
189 def allowed_actions
185 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
190 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
186 end
191 end
187
192
188 def check_deletable
193 def check_deletable
189 raise "Can't delete role" if members.any?
194 raise "Can't delete role" if members.any?
190 raise "Can't delete builtin role" if builtin?
195 raise "Can't delete builtin role" if builtin?
191 end
196 end
192
197
193 def self.find_or_create_system_role(builtin, name)
198 def self.find_or_create_system_role(builtin, name)
194 role = where(:builtin => builtin).first
199 role = where(:builtin => builtin).first
195 if role.nil?
200 if role.nil?
196 role = create(:name => name, :position => 0) do |r|
201 role = create(:name => name, :position => 0) do |r|
197 r.builtin = builtin
202 r.builtin = builtin
198 end
203 end
199 raise "Unable to create the #{name} role." if role.new_record?
204 raise "Unable to create the #{name} role." if role.new_record?
200 end
205 end
201 role
206 role
202 end
207 end
203 end
208 end
@@ -1,30 +1,32
1 <%= error_messages_for 'role' %>
1 <%= error_messages_for 'role' %>
2
2
3 <% unless @role.anonymous? %>
3 <div class="box tabular">
4 <div class="box tabular">
4 <% unless @role.builtin? %>
5 <% unless @role.builtin? %>
5 <p><%= f.text_field :name, :required => true %></p>
6 <p><%= f.text_field :name, :required => true %></p>
6 <p><%= f.check_box :assignable %></p>
7 <p><%= f.check_box :assignable %></p>
7 <% end %>
8 <% end %>
8 <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
9 <p><%= f.select :issues_visibility, Role::ISSUES_VISIBILITY_OPTIONS.collect {|v| [l(v.last), v.first]} %></p>
9 <% if @role.new_record? && @roles.any? %>
10 <% if @role.new_record? && @roles.any? %>
10 <p><label for="copy_workflow_from"><%= l(:label_copy_workflow_from) %></label>
11 <p><label for="copy_workflow_from"><%= l(:label_copy_workflow_from) %></label>
11 <%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from] || @copy_from.try(:id))) %></p>
12 <%= select_tag(:copy_workflow_from, content_tag("option") + options_from_collection_for_select(@roles, :id, :name, params[:copy_workflow_from] || @copy_from.try(:id))) %></p>
12 <% end %>
13 <% end %>
13 </div>
14 </div>
15 <% end %>
14
16
15 <h3><%= l(:label_permissions) %></h3>
17 <h3><%= l(:label_permissions) %></h3>
16 <div class="box tabular" id="permissions">
18 <div class="box tabular" id="permissions">
17 <% perms_by_module = @role.setable_permissions.group_by {|p| p.project_module.to_s} %>
19 <% perms_by_module = @role.setable_permissions.group_by {|p| p.project_module.to_s} %>
18 <% perms_by_module.keys.sort.each do |mod| %>
20 <% perms_by_module.keys.sort.each do |mod| %>
19 <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
21 <fieldset><legend><%= mod.blank? ? l(:label_project) : l_or_humanize(mod, :prefix => 'project_module_') %></legend>
20 <% perms_by_module[mod].each do |permission| %>
22 <% perms_by_module[mod].each do |permission| %>
21 <label class="floating">
23 <label class="floating">
22 <%= check_box_tag 'role[permissions][]', permission.name, (@role.permissions.include? permission.name) %>
24 <%= check_box_tag 'role[permissions][]', permission.name, (@role.permissions.include? permission.name) %>
23 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
25 <%= l_or_humanize(permission.name, :prefix => 'permission_') %>
24 </label>
26 </label>
25 <% end %>
27 <% end %>
26 </fieldset>
28 </fieldset>
27 <% end %>
29 <% end %>
28 <br /><%= check_all_links 'permissions' %>
30 <br /><%= check_all_links 'permissions' %>
29 <%= hidden_field_tag 'role[permissions][]', '' %>
31 <%= hidden_field_tag 'role[permissions][]', '' %>
30 </div>
32 </div>
@@ -1,211 +1,219
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
2 # Copyright (C) 2006-2012 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 require File.expand_path('../../test_helper', __FILE__)
18 require File.expand_path('../../test_helper', __FILE__)
19
19
20 class RolesControllerTest < ActionController::TestCase
20 class RolesControllerTest < ActionController::TestCase
21 fixtures :roles, :users, :members, :member_roles, :workflows, :trackers
21 fixtures :roles, :users, :members, :member_roles, :workflows, :trackers
22
22
23 def setup
23 def setup
24 @controller = RolesController.new
24 @controller = RolesController.new
25 @request = ActionController::TestRequest.new
25 @request = ActionController::TestRequest.new
26 @response = ActionController::TestResponse.new
26 @response = ActionController::TestResponse.new
27 User.current = nil
27 User.current = nil
28 @request.session[:user_id] = 1 # admin
28 @request.session[:user_id] = 1 # admin
29 end
29 end
30
30
31 def test_index
31 def test_index
32 get :index
32 get :index
33 assert_response :success
33 assert_response :success
34 assert_template 'index'
34 assert_template 'index'
35
35
36 assert_not_nil assigns(:roles)
36 assert_not_nil assigns(:roles)
37 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
37 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
38
38
39 assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' },
39 assert_tag :tag => 'a', :attributes => { :href => '/roles/1/edit' },
40 :content => 'Manager'
40 :content => 'Manager'
41 end
41 end
42
42
43 def test_new
43 def test_new
44 get :new
44 get :new
45 assert_response :success
45 assert_response :success
46 assert_template 'new'
46 assert_template 'new'
47 end
47 end
48
48
49 def test_new_with_copy
49 def test_new_with_copy
50 copy_from = Role.find(2)
50 copy_from = Role.find(2)
51
51
52 get :new, :copy => copy_from.id.to_s
52 get :new, :copy => copy_from.id.to_s
53 assert_response :success
53 assert_response :success
54 assert_template 'new'
54 assert_template 'new'
55
55
56 role = assigns(:role)
56 role = assigns(:role)
57 assert_equal copy_from.permissions, role.permissions
57 assert_equal copy_from.permissions, role.permissions
58
58
59 assert_select 'form' do
59 assert_select 'form' do
60 # blank name
60 # blank name
61 assert_select 'input[name=?][value=]', 'role[name]'
61 assert_select 'input[name=?][value=]', 'role[name]'
62 # edit_project permission checked
62 # edit_project permission checked
63 assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]'
63 assert_select 'input[type=checkbox][name=?][value=edit_project][checked=checked]', 'role[permissions][]'
64 # add_project permission not checked
64 # add_project permission not checked
65 assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]'
65 assert_select 'input[type=checkbox][name=?][value=add_project]', 'role[permissions][]'
66 assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0
66 assert_select 'input[type=checkbox][name=?][value=add_project][checked=checked]', 'role[permissions][]', 0
67 # workflow copy selected
67 # workflow copy selected
68 assert_select 'select[name=?]', 'copy_workflow_from' do
68 assert_select 'select[name=?]', 'copy_workflow_from' do
69 assert_select 'option[value=2][selected=selected]'
69 assert_select 'option[value=2][selected=selected]'
70 end
70 end
71 end
71 end
72 end
72 end
73
73
74 def test_create_with_validaton_failure
74 def test_create_with_validaton_failure
75 post :create, :role => {:name => '',
75 post :create, :role => {:name => '',
76 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
76 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
77 :assignable => '0'}
77 :assignable => '0'}
78
78
79 assert_response :success
79 assert_response :success
80 assert_template 'new'
80 assert_template 'new'
81 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' }
81 assert_tag :tag => 'div', :attributes => { :id => 'errorExplanation' }
82 end
82 end
83
83
84 def test_create_without_workflow_copy
84 def test_create_without_workflow_copy
85 post :create, :role => {:name => 'RoleWithoutWorkflowCopy',
85 post :create, :role => {:name => 'RoleWithoutWorkflowCopy',
86 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
86 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
87 :assignable => '0'}
87 :assignable => '0'}
88
88
89 assert_redirected_to '/roles'
89 assert_redirected_to '/roles'
90 role = Role.find_by_name('RoleWithoutWorkflowCopy')
90 role = Role.find_by_name('RoleWithoutWorkflowCopy')
91 assert_not_nil role
91 assert_not_nil role
92 assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
92 assert_equal [:add_issues, :edit_issues, :log_time], role.permissions
93 assert !role.assignable?
93 assert !role.assignable?
94 end
94 end
95
95
96 def test_create_with_workflow_copy
96 def test_create_with_workflow_copy
97 post :create, :role => {:name => 'RoleWithWorkflowCopy',
97 post :create, :role => {:name => 'RoleWithWorkflowCopy',
98 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
98 :permissions => ['add_issues', 'edit_issues', 'log_time', ''],
99 :assignable => '0'},
99 :assignable => '0'},
100 :copy_workflow_from => '1'
100 :copy_workflow_from => '1'
101
101
102 assert_redirected_to '/roles'
102 assert_redirected_to '/roles'
103 role = Role.find_by_name('RoleWithWorkflowCopy')
103 role = Role.find_by_name('RoleWithWorkflowCopy')
104 assert_not_nil role
104 assert_not_nil role
105 assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size
105 assert_equal Role.find(1).workflow_rules.size, role.workflow_rules.size
106 end
106 end
107
107
108 def test_edit
108 def test_edit
109 get :edit, :id => 1
109 get :edit, :id => 1
110 assert_response :success
110 assert_response :success
111 assert_template 'edit'
111 assert_template 'edit'
112 assert_equal Role.find(1), assigns(:role)
112 assert_equal Role.find(1), assigns(:role)
113 assert_select 'select[name=?]', 'role[issues_visibility]'
114 end
115
116 def test_edit_anonymous
117 get :edit, :id => Role.anonymous.id
118 assert_response :success
119 assert_template 'edit'
120 assert_select 'select[name=?]', 'role[issues_visibility]', 0
113 end
121 end
114
122
115 def test_edit_invalid_should_respond_with_404
123 def test_edit_invalid_should_respond_with_404
116 get :edit, :id => 999
124 get :edit, :id => 999
117 assert_response 404
125 assert_response 404
118 end
126 end
119
127
120 def test_update
128 def test_update
121 put :update, :id => 1,
129 put :update, :id => 1,
122 :role => {:name => 'Manager',
130 :role => {:name => 'Manager',
123 :permissions => ['edit_project', ''],
131 :permissions => ['edit_project', ''],
124 :assignable => '0'}
132 :assignable => '0'}
125
133
126 assert_redirected_to '/roles'
134 assert_redirected_to '/roles'
127 role = Role.find(1)
135 role = Role.find(1)
128 assert_equal [:edit_project], role.permissions
136 assert_equal [:edit_project], role.permissions
129 end
137 end
130
138
131 def test_update_with_failure
139 def test_update_with_failure
132 put :update, :id => 1, :role => {:name => ''}
140 put :update, :id => 1, :role => {:name => ''}
133 assert_response :success
141 assert_response :success
134 assert_template 'edit'
142 assert_template 'edit'
135 end
143 end
136
144
137 def test_destroy
145 def test_destroy
138 r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
146 r = Role.create!(:name => 'ToBeDestroyed', :permissions => [:view_wiki_pages])
139
147
140 delete :destroy, :id => r
148 delete :destroy, :id => r
141 assert_redirected_to '/roles'
149 assert_redirected_to '/roles'
142 assert_nil Role.find_by_id(r.id)
150 assert_nil Role.find_by_id(r.id)
143 end
151 end
144
152
145 def test_destroy_role_in_use
153 def test_destroy_role_in_use
146 delete :destroy, :id => 1
154 delete :destroy, :id => 1
147 assert_redirected_to '/roles'
155 assert_redirected_to '/roles'
148 assert_equal 'This role is in use and cannot be deleted.', flash[:error]
156 assert_equal 'This role is in use and cannot be deleted.', flash[:error]
149 assert_not_nil Role.find_by_id(1)
157 assert_not_nil Role.find_by_id(1)
150 end
158 end
151
159
152 def test_get_permissions
160 def test_get_permissions
153 get :permissions
161 get :permissions
154 assert_response :success
162 assert_response :success
155 assert_template 'permissions'
163 assert_template 'permissions'
156
164
157 assert_not_nil assigns(:roles)
165 assert_not_nil assigns(:roles)
158 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
166 assert_equal Role.find(:all, :order => 'builtin, position'), assigns(:roles)
159
167
160 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
168 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
161 :name => 'permissions[3][]',
169 :name => 'permissions[3][]',
162 :value => 'add_issues',
170 :value => 'add_issues',
163 :checked => 'checked' }
171 :checked => 'checked' }
164
172
165 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
173 assert_tag :tag => 'input', :attributes => { :type => 'checkbox',
166 :name => 'permissions[3][]',
174 :name => 'permissions[3][]',
167 :value => 'delete_issues',
175 :value => 'delete_issues',
168 :checked => nil }
176 :checked => nil }
169 end
177 end
170
178
171 def test_post_permissions
179 def test_post_permissions
172 post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
180 post :permissions, :permissions => { '0' => '', '1' => ['edit_issues'], '3' => ['add_issues', 'delete_issues']}
173 assert_redirected_to '/roles'
181 assert_redirected_to '/roles'
174
182
175 assert_equal [:edit_issues], Role.find(1).permissions
183 assert_equal [:edit_issues], Role.find(1).permissions
176 assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
184 assert_equal [:add_issues, :delete_issues], Role.find(3).permissions
177 assert Role.find(2).permissions.empty?
185 assert Role.find(2).permissions.empty?
178 end
186 end
179
187
180 def test_clear_all_permissions
188 def test_clear_all_permissions
181 post :permissions, :permissions => { '0' => '' }
189 post :permissions, :permissions => { '0' => '' }
182 assert_redirected_to '/roles'
190 assert_redirected_to '/roles'
183 assert Role.find(1).permissions.empty?
191 assert Role.find(1).permissions.empty?
184 end
192 end
185
193
186 def test_move_highest
194 def test_move_highest
187 put :update, :id => 3, :role => {:move_to => 'highest'}
195 put :update, :id => 3, :role => {:move_to => 'highest'}
188 assert_redirected_to '/roles'
196 assert_redirected_to '/roles'
189 assert_equal 1, Role.find(3).position
197 assert_equal 1, Role.find(3).position
190 end
198 end
191
199
192 def test_move_higher
200 def test_move_higher
193 position = Role.find(3).position
201 position = Role.find(3).position
194 put :update, :id => 3, :role => {:move_to => 'higher'}
202 put :update, :id => 3, :role => {:move_to => 'higher'}
195 assert_redirected_to '/roles'
203 assert_redirected_to '/roles'
196 assert_equal position - 1, Role.find(3).position
204 assert_equal position - 1, Role.find(3).position
197 end
205 end
198
206
199 def test_move_lower
207 def test_move_lower
200 position = Role.find(2).position
208 position = Role.find(2).position
201 put :update, :id => 2, :role => {:move_to => 'lower'}
209 put :update, :id => 2, :role => {:move_to => 'lower'}
202 assert_redirected_to '/roles'
210 assert_redirected_to '/roles'
203 assert_equal position + 1, Role.find(2).position
211 assert_equal position + 1, Role.find(2).position
204 end
212 end
205
213
206 def test_move_lowest
214 def test_move_lowest
207 put :update, :id => 2, :role => {:move_to => 'lowest'}
215 put :update, :id => 2, :role => {:move_to => 'lowest'}
208 assert_redirected_to '/roles'
216 assert_redirected_to '/roles'
209 assert_equal Role.count, Role.find(2).position
217 assert_equal Role.count, Role.find(2).position
210 end
218 end
211 end
219 end
General Comments 0
You need to be logged in to leave comments. Login now