##// END OF EJS Templates
Fixed that #reload raises a Stack too deep error with ruby 2.0....
Jean-Philippe Lang -
r11267:5c1e1ee4bb16
parent child
Show More
@@ -1,1423 +1,1424
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Issue < ActiveRecord::Base
18 class Issue < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20 include Redmine::Utils::DateCalculation
20 include Redmine::Utils::DateCalculation
21
21
22 belongs_to :project
22 belongs_to :project
23 belongs_to :tracker
23 belongs_to :tracker
24 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
24 belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
25 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
25 belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
26 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
26 belongs_to :assigned_to, :class_name => 'Principal', :foreign_key => 'assigned_to_id'
27 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
27 belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
28 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
28 belongs_to :priority, :class_name => 'IssuePriority', :foreign_key => 'priority_id'
29 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
29 belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
30
30
31 has_many :journals, :as => :journalized, :dependent => :destroy
31 has_many :journals, :as => :journalized, :dependent => :destroy
32 has_many :visible_journals,
32 has_many :visible_journals,
33 :class_name => 'Journal',
33 :class_name => 'Journal',
34 :as => :journalized,
34 :as => :journalized,
35 :conditions => Proc.new {
35 :conditions => Proc.new {
36 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
36 ["(#{Journal.table_name}.private_notes = ? OR (#{Project.allowed_to_condition(User.current, :view_private_notes)}))", false]
37 },
37 },
38 :readonly => true
38 :readonly => true
39
39
40 has_many :time_entries, :dependent => :delete_all
40 has_many :time_entries, :dependent => :delete_all
41 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
41 has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
42
42
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
43 has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
44 has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
45
45
46 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
46 acts_as_nested_set :scope => 'root_id', :dependent => :destroy
47 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
47 acts_as_attachable :after_add => :attachment_added, :after_remove => :attachment_removed
48 acts_as_customizable
48 acts_as_customizable
49 acts_as_watchable
49 acts_as_watchable
50 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
50 acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
51 :include => [:project, :visible_journals],
51 :include => [:project, :visible_journals],
52 # sort by id so that limited eager loading doesn't break with postgresql
52 # sort by id so that limited eager loading doesn't break with postgresql
53 :order_column => "#{table_name}.id"
53 :order_column => "#{table_name}.id"
54 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
54 acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
55 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
55 :url => Proc.new {|o| {:controller => 'issues', :action => 'show', :id => o.id}},
56 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
56 :type => Proc.new {|o| 'issue' + (o.closed? ? ' closed' : '') }
57
57
58 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
58 acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
59 :author_key => :author_id
59 :author_key => :author_id
60
60
61 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
61 DONE_RATIO_OPTIONS = %w(issue_field issue_status)
62
62
63 attr_reader :current_journal
63 attr_reader :current_journal
64 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
64 delegate :notes, :notes=, :private_notes, :private_notes=, :to => :current_journal, :allow_nil => true
65
65
66 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
66 validates_presence_of :subject, :priority, :project, :tracker, :author, :status
67
67
68 validates_length_of :subject, :maximum => 255
68 validates_length_of :subject, :maximum => 255
69 validates_inclusion_of :done_ratio, :in => 0..100
69 validates_inclusion_of :done_ratio, :in => 0..100
70 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
70 validates :estimated_hours, :numericality => {:greater_than_or_equal_to => 0, :allow_nil => true, :message => :invalid}
71 validates :start_date, :date => true
71 validates :start_date, :date => true
72 validates :due_date, :date => true
72 validates :due_date, :date => true
73 validate :validate_issue, :validate_required_fields
73 validate :validate_issue, :validate_required_fields
74
74
75 scope :visible, lambda {|*args|
75 scope :visible, lambda {|*args|
76 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
76 includes(:project).where(Issue.visible_condition(args.shift || User.current, *args))
77 }
77 }
78
78
79 scope :open, lambda {|*args|
79 scope :open, lambda {|*args|
80 is_closed = args.size > 0 ? !args.first : false
80 is_closed = args.size > 0 ? !args.first : false
81 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
81 includes(:status).where("#{IssueStatus.table_name}.is_closed = ?", is_closed)
82 }
82 }
83
83
84 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
84 scope :recently_updated, lambda { order("#{Issue.table_name}.updated_on DESC") }
85 scope :on_active_project, lambda {
85 scope :on_active_project, lambda {
86 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
86 includes(:status, :project, :tracker).where("#{Project.table_name}.status = ?", Project::STATUS_ACTIVE)
87 }
87 }
88 scope :fixed_version, lambda {|versions|
88 scope :fixed_version, lambda {|versions|
89 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
89 ids = [versions].flatten.compact.map {|v| v.is_a?(Version) ? v.id : v}
90 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
90 ids.any? ? where(:fixed_version_id => ids) : where('1=0')
91 }
91 }
92
92
93 before_create :default_assign
93 before_create :default_assign
94 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
94 before_save :close_duplicates, :update_done_ratio_from_issue_status, :force_updated_on_change, :update_closed_on
95 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
95 after_save {|issue| issue.send :after_project_change if !issue.id_changed? && issue.project_id_changed?}
96 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
96 after_save :reschedule_following_issues, :update_nested_set_attributes, :update_parent_attributes, :create_journal
97 # Should be after_create but would be called before previous after_save callbacks
97 # Should be after_create but would be called before previous after_save callbacks
98 after_save :after_create_from_copy
98 after_save :after_create_from_copy
99 after_destroy :update_parent_attributes
99 after_destroy :update_parent_attributes
100
100
101 # Returns a SQL conditions string used to find all issues visible by the specified user
101 # Returns a SQL conditions string used to find all issues visible by the specified user
102 def self.visible_condition(user, options={})
102 def self.visible_condition(user, options={})
103 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
103 Project.allowed_to_condition(user, :view_issues, options) do |role, user|
104 if user.logged?
104 if user.logged?
105 case role.issues_visibility
105 case role.issues_visibility
106 when 'all'
106 when 'all'
107 nil
107 nil
108 when 'default'
108 when 'default'
109 user_ids = [user.id] + user.groups.map(&:id)
109 user_ids = [user.id] + user.groups.map(&:id)
110 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
110 "(#{table_name}.is_private = #{connection.quoted_false} OR #{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
111 when 'own'
111 when 'own'
112 user_ids = [user.id] + user.groups.map(&:id)
112 user_ids = [user.id] + user.groups.map(&:id)
113 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
113 "(#{table_name}.author_id = #{user.id} OR #{table_name}.assigned_to_id IN (#{user_ids.join(',')}))"
114 else
114 else
115 '1=0'
115 '1=0'
116 end
116 end
117 else
117 else
118 "(#{table_name}.is_private = #{connection.quoted_false})"
118 "(#{table_name}.is_private = #{connection.quoted_false})"
119 end
119 end
120 end
120 end
121 end
121 end
122
122
123 # Returns true if usr or current user is allowed to view the issue
123 # Returns true if usr or current user is allowed to view the issue
124 def visible?(usr=nil)
124 def visible?(usr=nil)
125 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
125 (usr || User.current).allowed_to?(:view_issues, self.project) do |role, user|
126 if user.logged?
126 if user.logged?
127 case role.issues_visibility
127 case role.issues_visibility
128 when 'all'
128 when 'all'
129 true
129 true
130 when 'default'
130 when 'default'
131 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
131 !self.is_private? || (self.author == user || user.is_or_belongs_to?(assigned_to))
132 when 'own'
132 when 'own'
133 self.author == user || user.is_or_belongs_to?(assigned_to)
133 self.author == user || user.is_or_belongs_to?(assigned_to)
134 else
134 else
135 false
135 false
136 end
136 end
137 else
137 else
138 !self.is_private?
138 !self.is_private?
139 end
139 end
140 end
140 end
141 end
141 end
142
142
143 # Returns true if user or current user is allowed to edit or add a note to the issue
143 # Returns true if user or current user is allowed to edit or add a note to the issue
144 def editable?(user=User.current)
144 def editable?(user=User.current)
145 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
145 user.allowed_to?(:edit_issues, project) || user.allowed_to?(:add_issue_notes, project)
146 end
146 end
147
147
148 def initialize(attributes=nil, *args)
148 def initialize(attributes=nil, *args)
149 super
149 super
150 if new_record?
150 if new_record?
151 # set default values for new records only
151 # set default values for new records only
152 self.status ||= IssueStatus.default
152 self.status ||= IssueStatus.default
153 self.priority ||= IssuePriority.default
153 self.priority ||= IssuePriority.default
154 self.watcher_user_ids = []
154 self.watcher_user_ids = []
155 end
155 end
156 end
156 end
157
157
158 def create_or_update
158 def create_or_update
159 super
159 super
160 ensure
160 ensure
161 @status_was = nil
161 @status_was = nil
162 end
162 end
163 private :create_or_update
163 private :create_or_update
164
164
165 # AR#Persistence#destroy would raise and RecordNotFound exception
165 # AR#Persistence#destroy would raise and RecordNotFound exception
166 # if the issue was already deleted or updated (non matching lock_version).
166 # if the issue was already deleted or updated (non matching lock_version).
167 # This is a problem when bulk deleting issues or deleting a project
167 # This is a problem when bulk deleting issues or deleting a project
168 # (because an issue may already be deleted if its parent was deleted
168 # (because an issue may already be deleted if its parent was deleted
169 # first).
169 # first).
170 # The issue is reloaded by the nested_set before being deleted so
170 # The issue is reloaded by the nested_set before being deleted so
171 # the lock_version condition should not be an issue but we handle it.
171 # the lock_version condition should not be an issue but we handle it.
172 def destroy
172 def destroy
173 super
173 super
174 rescue ActiveRecord::RecordNotFound
174 rescue ActiveRecord::RecordNotFound
175 # Stale or already deleted
175 # Stale or already deleted
176 begin
176 begin
177 reload
177 reload
178 rescue ActiveRecord::RecordNotFound
178 rescue ActiveRecord::RecordNotFound
179 # The issue was actually already deleted
179 # The issue was actually already deleted
180 @destroyed = true
180 @destroyed = true
181 return freeze
181 return freeze
182 end
182 end
183 # The issue was stale, retry to destroy
183 # The issue was stale, retry to destroy
184 super
184 super
185 end
185 end
186
186
187 alias :base_reload :reload
187 def reload(*args)
188 def reload(*args)
188 @workflow_rule_by_attribute = nil
189 @workflow_rule_by_attribute = nil
189 @assignable_versions = nil
190 @assignable_versions = nil
190 @relations = nil
191 @relations = nil
191 super
192 base_reload(*args)
192 end
193 end
193
194
194 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
195 # Overrides Redmine::Acts::Customizable::InstanceMethods#available_custom_fields
195 def available_custom_fields
196 def available_custom_fields
196 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
197 (project && tracker) ? (project.all_issue_custom_fields & tracker.custom_fields.all) : []
197 end
198 end
198
199
199 # Copies attributes from another issue, arg can be an id or an Issue
200 # Copies attributes from another issue, arg can be an id or an Issue
200 def copy_from(arg, options={})
201 def copy_from(arg, options={})
201 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
202 issue = arg.is_a?(Issue) ? arg : Issue.visible.find(arg)
202 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
203 self.attributes = issue.attributes.dup.except("id", "root_id", "parent_id", "lft", "rgt", "created_on", "updated_on")
203 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
204 self.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
204 self.status = issue.status
205 self.status = issue.status
205 self.author = User.current
206 self.author = User.current
206 unless options[:attachments] == false
207 unless options[:attachments] == false
207 self.attachments = issue.attachments.map do |attachement|
208 self.attachments = issue.attachments.map do |attachement|
208 attachement.copy(:container => self)
209 attachement.copy(:container => self)
209 end
210 end
210 end
211 end
211 @copied_from = issue
212 @copied_from = issue
212 @copy_options = options
213 @copy_options = options
213 self
214 self
214 end
215 end
215
216
216 # Returns an unsaved copy of the issue
217 # Returns an unsaved copy of the issue
217 def copy(attributes=nil, copy_options={})
218 def copy(attributes=nil, copy_options={})
218 copy = self.class.new.copy_from(self, copy_options)
219 copy = self.class.new.copy_from(self, copy_options)
219 copy.attributes = attributes if attributes
220 copy.attributes = attributes if attributes
220 copy
221 copy
221 end
222 end
222
223
223 # Returns true if the issue is a copy
224 # Returns true if the issue is a copy
224 def copy?
225 def copy?
225 @copied_from.present?
226 @copied_from.present?
226 end
227 end
227
228
228 # Moves/copies an issue to a new project and tracker
229 # Moves/copies an issue to a new project and tracker
229 # Returns the moved/copied issue on success, false on failure
230 # Returns the moved/copied issue on success, false on failure
230 def move_to_project(new_project, new_tracker=nil, options={})
231 def move_to_project(new_project, new_tracker=nil, options={})
231 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
232 ActiveSupport::Deprecation.warn "Issue#move_to_project is deprecated, use #project= instead."
232
233
233 if options[:copy]
234 if options[:copy]
234 issue = self.copy
235 issue = self.copy
235 else
236 else
236 issue = self
237 issue = self
237 end
238 end
238
239
239 issue.init_journal(User.current, options[:notes])
240 issue.init_journal(User.current, options[:notes])
240
241
241 # Preserve previous behaviour
242 # Preserve previous behaviour
242 # #move_to_project doesn't change tracker automatically
243 # #move_to_project doesn't change tracker automatically
243 issue.send :project=, new_project, true
244 issue.send :project=, new_project, true
244 if new_tracker
245 if new_tracker
245 issue.tracker = new_tracker
246 issue.tracker = new_tracker
246 end
247 end
247 # Allow bulk setting of attributes on the issue
248 # Allow bulk setting of attributes on the issue
248 if options[:attributes]
249 if options[:attributes]
249 issue.attributes = options[:attributes]
250 issue.attributes = options[:attributes]
250 end
251 end
251
252
252 issue.save ? issue : false
253 issue.save ? issue : false
253 end
254 end
254
255
255 def status_id=(sid)
256 def status_id=(sid)
256 self.status = nil
257 self.status = nil
257 result = write_attribute(:status_id, sid)
258 result = write_attribute(:status_id, sid)
258 @workflow_rule_by_attribute = nil
259 @workflow_rule_by_attribute = nil
259 result
260 result
260 end
261 end
261
262
262 def priority_id=(pid)
263 def priority_id=(pid)
263 self.priority = nil
264 self.priority = nil
264 write_attribute(:priority_id, pid)
265 write_attribute(:priority_id, pid)
265 end
266 end
266
267
267 def category_id=(cid)
268 def category_id=(cid)
268 self.category = nil
269 self.category = nil
269 write_attribute(:category_id, cid)
270 write_attribute(:category_id, cid)
270 end
271 end
271
272
272 def fixed_version_id=(vid)
273 def fixed_version_id=(vid)
273 self.fixed_version = nil
274 self.fixed_version = nil
274 write_attribute(:fixed_version_id, vid)
275 write_attribute(:fixed_version_id, vid)
275 end
276 end
276
277
277 def tracker_id=(tid)
278 def tracker_id=(tid)
278 self.tracker = nil
279 self.tracker = nil
279 result = write_attribute(:tracker_id, tid)
280 result = write_attribute(:tracker_id, tid)
280 @custom_field_values = nil
281 @custom_field_values = nil
281 @workflow_rule_by_attribute = nil
282 @workflow_rule_by_attribute = nil
282 result
283 result
283 end
284 end
284
285
285 def project_id=(project_id)
286 def project_id=(project_id)
286 if project_id.to_s != self.project_id.to_s
287 if project_id.to_s != self.project_id.to_s
287 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
288 self.project = (project_id.present? ? Project.find_by_id(project_id) : nil)
288 end
289 end
289 end
290 end
290
291
291 def project=(project, keep_tracker=false)
292 def project=(project, keep_tracker=false)
292 project_was = self.project
293 project_was = self.project
293 write_attribute(:project_id, project ? project.id : nil)
294 write_attribute(:project_id, project ? project.id : nil)
294 association_instance_set('project', project)
295 association_instance_set('project', project)
295 if project_was && project && project_was != project
296 if project_was && project && project_was != project
296 @assignable_versions = nil
297 @assignable_versions = nil
297
298
298 unless keep_tracker || project.trackers.include?(tracker)
299 unless keep_tracker || project.trackers.include?(tracker)
299 self.tracker = project.trackers.first
300 self.tracker = project.trackers.first
300 end
301 end
301 # Reassign to the category with same name if any
302 # Reassign to the category with same name if any
302 if category
303 if category
303 self.category = project.issue_categories.find_by_name(category.name)
304 self.category = project.issue_categories.find_by_name(category.name)
304 end
305 end
305 # Keep the fixed_version if it's still valid in the new_project
306 # Keep the fixed_version if it's still valid in the new_project
306 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
307 if fixed_version && fixed_version.project != project && !project.shared_versions.include?(fixed_version)
307 self.fixed_version = nil
308 self.fixed_version = nil
308 end
309 end
309 # Clear the parent task if it's no longer valid
310 # Clear the parent task if it's no longer valid
310 unless valid_parent_project?
311 unless valid_parent_project?
311 self.parent_issue_id = nil
312 self.parent_issue_id = nil
312 end
313 end
313 @custom_field_values = nil
314 @custom_field_values = nil
314 end
315 end
315 end
316 end
316
317
317 def description=(arg)
318 def description=(arg)
318 if arg.is_a?(String)
319 if arg.is_a?(String)
319 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
320 arg = arg.gsub(/(\r\n|\n|\r)/, "\r\n")
320 end
321 end
321 write_attribute(:description, arg)
322 write_attribute(:description, arg)
322 end
323 end
323
324
324 # Overrides assign_attributes so that project and tracker get assigned first
325 # Overrides assign_attributes so that project and tracker get assigned first
325 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
326 def assign_attributes_with_project_and_tracker_first(new_attributes, *args)
326 return if new_attributes.nil?
327 return if new_attributes.nil?
327 attrs = new_attributes.dup
328 attrs = new_attributes.dup
328 attrs.stringify_keys!
329 attrs.stringify_keys!
329
330
330 %w(project project_id tracker tracker_id).each do |attr|
331 %w(project project_id tracker tracker_id).each do |attr|
331 if attrs.has_key?(attr)
332 if attrs.has_key?(attr)
332 send "#{attr}=", attrs.delete(attr)
333 send "#{attr}=", attrs.delete(attr)
333 end
334 end
334 end
335 end
335 send :assign_attributes_without_project_and_tracker_first, attrs, *args
336 send :assign_attributes_without_project_and_tracker_first, attrs, *args
336 end
337 end
337 # Do not redefine alias chain on reload (see #4838)
338 # Do not redefine alias chain on reload (see #4838)
338 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
339 alias_method_chain(:assign_attributes, :project_and_tracker_first) unless method_defined?(:assign_attributes_without_project_and_tracker_first)
339
340
340 def estimated_hours=(h)
341 def estimated_hours=(h)
341 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
342 write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
342 end
343 end
343
344
344 safe_attributes 'project_id',
345 safe_attributes 'project_id',
345 :if => lambda {|issue, user|
346 :if => lambda {|issue, user|
346 if issue.new_record?
347 if issue.new_record?
347 issue.copy?
348 issue.copy?
348 elsif user.allowed_to?(:move_issues, issue.project)
349 elsif user.allowed_to?(:move_issues, issue.project)
349 projects = Issue.allowed_target_projects_on_move(user)
350 projects = Issue.allowed_target_projects_on_move(user)
350 projects.include?(issue.project) && projects.size > 1
351 projects.include?(issue.project) && projects.size > 1
351 end
352 end
352 }
353 }
353
354
354 safe_attributes 'tracker_id',
355 safe_attributes 'tracker_id',
355 'status_id',
356 'status_id',
356 'category_id',
357 'category_id',
357 'assigned_to_id',
358 'assigned_to_id',
358 'priority_id',
359 'priority_id',
359 'fixed_version_id',
360 'fixed_version_id',
360 'subject',
361 'subject',
361 'description',
362 'description',
362 'start_date',
363 'start_date',
363 'due_date',
364 'due_date',
364 'done_ratio',
365 'done_ratio',
365 'estimated_hours',
366 'estimated_hours',
366 'custom_field_values',
367 'custom_field_values',
367 'custom_fields',
368 'custom_fields',
368 'lock_version',
369 'lock_version',
369 'notes',
370 'notes',
370 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
371 :if => lambda {|issue, user| issue.new_record? || user.allowed_to?(:edit_issues, issue.project) }
371
372
372 safe_attributes 'status_id',
373 safe_attributes 'status_id',
373 'assigned_to_id',
374 'assigned_to_id',
374 'fixed_version_id',
375 'fixed_version_id',
375 'done_ratio',
376 'done_ratio',
376 'lock_version',
377 'lock_version',
377 'notes',
378 'notes',
378 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
379 :if => lambda {|issue, user| issue.new_statuses_allowed_to(user).any? }
379
380
380 safe_attributes 'notes',
381 safe_attributes 'notes',
381 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
382 :if => lambda {|issue, user| user.allowed_to?(:add_issue_notes, issue.project)}
382
383
383 safe_attributes 'private_notes',
384 safe_attributes 'private_notes',
384 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
385 :if => lambda {|issue, user| !issue.new_record? && user.allowed_to?(:set_notes_private, issue.project)}
385
386
386 safe_attributes 'watcher_user_ids',
387 safe_attributes 'watcher_user_ids',
387 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
388 :if => lambda {|issue, user| issue.new_record? && user.allowed_to?(:add_issue_watchers, issue.project)}
388
389
389 safe_attributes 'is_private',
390 safe_attributes 'is_private',
390 :if => lambda {|issue, user|
391 :if => lambda {|issue, user|
391 user.allowed_to?(:set_issues_private, issue.project) ||
392 user.allowed_to?(:set_issues_private, issue.project) ||
392 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
393 (issue.author == user && user.allowed_to?(:set_own_issues_private, issue.project))
393 }
394 }
394
395
395 safe_attributes 'parent_issue_id',
396 safe_attributes 'parent_issue_id',
396 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
397 :if => lambda {|issue, user| (issue.new_record? || user.allowed_to?(:edit_issues, issue.project)) &&
397 user.allowed_to?(:manage_subtasks, issue.project)}
398 user.allowed_to?(:manage_subtasks, issue.project)}
398
399
399 def safe_attribute_names(user=nil)
400 def safe_attribute_names(user=nil)
400 names = super
401 names = super
401 names -= disabled_core_fields
402 names -= disabled_core_fields
402 names -= read_only_attribute_names(user)
403 names -= read_only_attribute_names(user)
403 names
404 names
404 end
405 end
405
406
406 # Safely sets attributes
407 # Safely sets attributes
407 # Should be called from controllers instead of #attributes=
408 # Should be called from controllers instead of #attributes=
408 # attr_accessible is too rough because we still want things like
409 # attr_accessible is too rough because we still want things like
409 # Issue.new(:project => foo) to work
410 # Issue.new(:project => foo) to work
410 def safe_attributes=(attrs, user=User.current)
411 def safe_attributes=(attrs, user=User.current)
411 return unless attrs.is_a?(Hash)
412 return unless attrs.is_a?(Hash)
412
413
413 attrs = attrs.dup
414 attrs = attrs.dup
414
415
415 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
416 # Project and Tracker must be set before since new_statuses_allowed_to depends on it.
416 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
417 if (p = attrs.delete('project_id')) && safe_attribute?('project_id')
417 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
418 if allowed_target_projects(user).collect(&:id).include?(p.to_i)
418 self.project_id = p
419 self.project_id = p
419 end
420 end
420 end
421 end
421
422
422 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
423 if (t = attrs.delete('tracker_id')) && safe_attribute?('tracker_id')
423 self.tracker_id = t
424 self.tracker_id = t
424 end
425 end
425
426
426 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
427 if (s = attrs.delete('status_id')) && safe_attribute?('status_id')
427 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
428 if new_statuses_allowed_to(user).collect(&:id).include?(s.to_i)
428 self.status_id = s
429 self.status_id = s
429 end
430 end
430 end
431 end
431
432
432 attrs = delete_unsafe_attributes(attrs, user)
433 attrs = delete_unsafe_attributes(attrs, user)
433 return if attrs.empty?
434 return if attrs.empty?
434
435
435 unless leaf?
436 unless leaf?
436 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
437 attrs.reject! {|k,v| %w(priority_id done_ratio start_date due_date estimated_hours).include?(k)}
437 end
438 end
438
439
439 if attrs['parent_issue_id'].present?
440 if attrs['parent_issue_id'].present?
440 s = attrs['parent_issue_id'].to_s
441 s = attrs['parent_issue_id'].to_s
441 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
442 unless (m = s.match(%r{\A#?(\d+)\z})) && (m[1] == parent_id.to_s || Issue.visible(user).exists?(m[1]))
442 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
443 @invalid_parent_issue_id = attrs.delete('parent_issue_id')
443 end
444 end
444 end
445 end
445
446
446 if attrs['custom_field_values'].present?
447 if attrs['custom_field_values'].present?
447 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
448 attrs['custom_field_values'] = attrs['custom_field_values'].reject {|k, v| read_only_attribute_names(user).include? k.to_s}
448 end
449 end
449
450
450 if attrs['custom_fields'].present?
451 if attrs['custom_fields'].present?
451 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
452 attrs['custom_fields'] = attrs['custom_fields'].reject {|c| read_only_attribute_names(user).include? c['id'].to_s}
452 end
453 end
453
454
454 # mass-assignment security bypass
455 # mass-assignment security bypass
455 assign_attributes attrs, :without_protection => true
456 assign_attributes attrs, :without_protection => true
456 end
457 end
457
458
458 def disabled_core_fields
459 def disabled_core_fields
459 tracker ? tracker.disabled_core_fields : []
460 tracker ? tracker.disabled_core_fields : []
460 end
461 end
461
462
462 # Returns the custom_field_values that can be edited by the given user
463 # Returns the custom_field_values that can be edited by the given user
463 def editable_custom_field_values(user=nil)
464 def editable_custom_field_values(user=nil)
464 custom_field_values.reject do |value|
465 custom_field_values.reject do |value|
465 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
466 read_only_attribute_names(user).include?(value.custom_field_id.to_s)
466 end
467 end
467 end
468 end
468
469
469 # Returns the names of attributes that are read-only for user or the current user
470 # Returns the names of attributes that are read-only for user or the current user
470 # For users with multiple roles, the read-only fields are the intersection of
471 # For users with multiple roles, the read-only fields are the intersection of
471 # read-only fields of each role
472 # read-only fields of each role
472 # The result is an array of strings where sustom fields are represented with their ids
473 # The result is an array of strings where sustom fields are represented with their ids
473 #
474 #
474 # Examples:
475 # Examples:
475 # issue.read_only_attribute_names # => ['due_date', '2']
476 # issue.read_only_attribute_names # => ['due_date', '2']
476 # issue.read_only_attribute_names(user) # => []
477 # issue.read_only_attribute_names(user) # => []
477 def read_only_attribute_names(user=nil)
478 def read_only_attribute_names(user=nil)
478 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
479 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'readonly'}.keys
479 end
480 end
480
481
481 # Returns the names of required attributes for user or the current user
482 # Returns the names of required attributes for user or the current user
482 # For users with multiple roles, the required fields are the intersection of
483 # For users with multiple roles, the required fields are the intersection of
483 # required fields of each role
484 # required fields of each role
484 # The result is an array of strings where sustom fields are represented with their ids
485 # The result is an array of strings where sustom fields are represented with their ids
485 #
486 #
486 # Examples:
487 # Examples:
487 # issue.required_attribute_names # => ['due_date', '2']
488 # issue.required_attribute_names # => ['due_date', '2']
488 # issue.required_attribute_names(user) # => []
489 # issue.required_attribute_names(user) # => []
489 def required_attribute_names(user=nil)
490 def required_attribute_names(user=nil)
490 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
491 workflow_rule_by_attribute(user).reject {|attr, rule| rule != 'required'}.keys
491 end
492 end
492
493
493 # Returns true if the attribute is required for user
494 # Returns true if the attribute is required for user
494 def required_attribute?(name, user=nil)
495 def required_attribute?(name, user=nil)
495 required_attribute_names(user).include?(name.to_s)
496 required_attribute_names(user).include?(name.to_s)
496 end
497 end
497
498
498 # Returns a hash of the workflow rule by attribute for the given user
499 # Returns a hash of the workflow rule by attribute for the given user
499 #
500 #
500 # Examples:
501 # Examples:
501 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
502 # issue.workflow_rule_by_attribute # => {'due_date' => 'required', 'start_date' => 'readonly'}
502 def workflow_rule_by_attribute(user=nil)
503 def workflow_rule_by_attribute(user=nil)
503 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
504 return @workflow_rule_by_attribute if @workflow_rule_by_attribute && user.nil?
504
505
505 user_real = user || User.current
506 user_real = user || User.current
506 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
507 roles = user_real.admin ? Role.all : user_real.roles_for_project(project)
507 return {} if roles.empty?
508 return {} if roles.empty?
508
509
509 result = {}
510 result = {}
510 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
511 workflow_permissions = WorkflowPermission.where(:tracker_id => tracker_id, :old_status_id => status_id, :role_id => roles.map(&:id)).all
511 if workflow_permissions.any?
512 if workflow_permissions.any?
512 workflow_rules = workflow_permissions.inject({}) do |h, wp|
513 workflow_rules = workflow_permissions.inject({}) do |h, wp|
513 h[wp.field_name] ||= []
514 h[wp.field_name] ||= []
514 h[wp.field_name] << wp.rule
515 h[wp.field_name] << wp.rule
515 h
516 h
516 end
517 end
517 workflow_rules.each do |attr, rules|
518 workflow_rules.each do |attr, rules|
518 next if rules.size < roles.size
519 next if rules.size < roles.size
519 uniq_rules = rules.uniq
520 uniq_rules = rules.uniq
520 if uniq_rules.size == 1
521 if uniq_rules.size == 1
521 result[attr] = uniq_rules.first
522 result[attr] = uniq_rules.first
522 else
523 else
523 result[attr] = 'required'
524 result[attr] = 'required'
524 end
525 end
525 end
526 end
526 end
527 end
527 @workflow_rule_by_attribute = result if user.nil?
528 @workflow_rule_by_attribute = result if user.nil?
528 result
529 result
529 end
530 end
530 private :workflow_rule_by_attribute
531 private :workflow_rule_by_attribute
531
532
532 def done_ratio
533 def done_ratio
533 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
534 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
534 status.default_done_ratio
535 status.default_done_ratio
535 else
536 else
536 read_attribute(:done_ratio)
537 read_attribute(:done_ratio)
537 end
538 end
538 end
539 end
539
540
540 def self.use_status_for_done_ratio?
541 def self.use_status_for_done_ratio?
541 Setting.issue_done_ratio == 'issue_status'
542 Setting.issue_done_ratio == 'issue_status'
542 end
543 end
543
544
544 def self.use_field_for_done_ratio?
545 def self.use_field_for_done_ratio?
545 Setting.issue_done_ratio == 'issue_field'
546 Setting.issue_done_ratio == 'issue_field'
546 end
547 end
547
548
548 def validate_issue
549 def validate_issue
549 if due_date && start_date && due_date < start_date
550 if due_date && start_date && due_date < start_date
550 errors.add :due_date, :greater_than_start_date
551 errors.add :due_date, :greater_than_start_date
551 end
552 end
552
553
553 if start_date && soonest_start && start_date < soonest_start
554 if start_date && soonest_start && start_date < soonest_start
554 errors.add :start_date, :invalid
555 errors.add :start_date, :invalid
555 end
556 end
556
557
557 if fixed_version
558 if fixed_version
558 if !assignable_versions.include?(fixed_version)
559 if !assignable_versions.include?(fixed_version)
559 errors.add :fixed_version_id, :inclusion
560 errors.add :fixed_version_id, :inclusion
560 elsif reopened? && fixed_version.closed?
561 elsif reopened? && fixed_version.closed?
561 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
562 errors.add :base, I18n.t(:error_can_not_reopen_issue_on_closed_version)
562 end
563 end
563 end
564 end
564
565
565 # Checks that the issue can not be added/moved to a disabled tracker
566 # Checks that the issue can not be added/moved to a disabled tracker
566 if project && (tracker_id_changed? || project_id_changed?)
567 if project && (tracker_id_changed? || project_id_changed?)
567 unless project.trackers.include?(tracker)
568 unless project.trackers.include?(tracker)
568 errors.add :tracker_id, :inclusion
569 errors.add :tracker_id, :inclusion
569 end
570 end
570 end
571 end
571
572
572 # Checks parent issue assignment
573 # Checks parent issue assignment
573 if @invalid_parent_issue_id.present?
574 if @invalid_parent_issue_id.present?
574 errors.add :parent_issue_id, :invalid
575 errors.add :parent_issue_id, :invalid
575 elsif @parent_issue
576 elsif @parent_issue
576 if !valid_parent_project?(@parent_issue)
577 if !valid_parent_project?(@parent_issue)
577 errors.add :parent_issue_id, :invalid
578 errors.add :parent_issue_id, :invalid
578 elsif !new_record?
579 elsif !new_record?
579 # moving an existing issue
580 # moving an existing issue
580 if @parent_issue.root_id != root_id
581 if @parent_issue.root_id != root_id
581 # we can always move to another tree
582 # we can always move to another tree
582 elsif move_possible?(@parent_issue)
583 elsif move_possible?(@parent_issue)
583 # move accepted inside tree
584 # move accepted inside tree
584 else
585 else
585 errors.add :parent_issue_id, :invalid
586 errors.add :parent_issue_id, :invalid
586 end
587 end
587 end
588 end
588 end
589 end
589 end
590 end
590
591
591 # Validates the issue against additional workflow requirements
592 # Validates the issue against additional workflow requirements
592 def validate_required_fields
593 def validate_required_fields
593 user = new_record? ? author : current_journal.try(:user)
594 user = new_record? ? author : current_journal.try(:user)
594
595
595 required_attribute_names(user).each do |attribute|
596 required_attribute_names(user).each do |attribute|
596 if attribute =~ /^\d+$/
597 if attribute =~ /^\d+$/
597 attribute = attribute.to_i
598 attribute = attribute.to_i
598 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
599 v = custom_field_values.detect {|v| v.custom_field_id == attribute }
599 if v && v.value.blank?
600 if v && v.value.blank?
600 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
601 errors.add :base, v.custom_field.name + ' ' + l('activerecord.errors.messages.blank')
601 end
602 end
602 else
603 else
603 if respond_to?(attribute) && send(attribute).blank?
604 if respond_to?(attribute) && send(attribute).blank?
604 errors.add attribute, :blank
605 errors.add attribute, :blank
605 end
606 end
606 end
607 end
607 end
608 end
608 end
609 end
609
610
610 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
611 # Set the done_ratio using the status if that setting is set. This will keep the done_ratios
611 # even if the user turns off the setting later
612 # even if the user turns off the setting later
612 def update_done_ratio_from_issue_status
613 def update_done_ratio_from_issue_status
613 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
614 if Issue.use_status_for_done_ratio? && status && status.default_done_ratio
614 self.done_ratio = status.default_done_ratio
615 self.done_ratio = status.default_done_ratio
615 end
616 end
616 end
617 end
617
618
618 def init_journal(user, notes = "")
619 def init_journal(user, notes = "")
619 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
620 @current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
620 if new_record?
621 if new_record?
621 @current_journal.notify = false
622 @current_journal.notify = false
622 else
623 else
623 @attributes_before_change = attributes.dup
624 @attributes_before_change = attributes.dup
624 @custom_values_before_change = {}
625 @custom_values_before_change = {}
625 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
626 self.custom_field_values.each {|c| @custom_values_before_change.store c.custom_field_id, c.value }
626 end
627 end
627 @current_journal
628 @current_journal
628 end
629 end
629
630
630 # Returns the id of the last journal or nil
631 # Returns the id of the last journal or nil
631 def last_journal_id
632 def last_journal_id
632 if new_record?
633 if new_record?
633 nil
634 nil
634 else
635 else
635 journals.maximum(:id)
636 journals.maximum(:id)
636 end
637 end
637 end
638 end
638
639
639 # Returns a scope for journals that have an id greater than journal_id
640 # Returns a scope for journals that have an id greater than journal_id
640 def journals_after(journal_id)
641 def journals_after(journal_id)
641 scope = journals.reorder("#{Journal.table_name}.id ASC")
642 scope = journals.reorder("#{Journal.table_name}.id ASC")
642 if journal_id.present?
643 if journal_id.present?
643 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
644 scope = scope.where("#{Journal.table_name}.id > ?", journal_id.to_i)
644 end
645 end
645 scope
646 scope
646 end
647 end
647
648
648 # Returns the initial status of the issue
649 # Returns the initial status of the issue
649 # Returns nil for a new issue
650 # Returns nil for a new issue
650 def status_was
651 def status_was
651 if status_id_was && status_id_was.to_i > 0
652 if status_id_was && status_id_was.to_i > 0
652 @status_was ||= IssueStatus.find_by_id(status_id_was)
653 @status_was ||= IssueStatus.find_by_id(status_id_was)
653 end
654 end
654 end
655 end
655
656
656 # Return true if the issue is closed, otherwise false
657 # Return true if the issue is closed, otherwise false
657 def closed?
658 def closed?
658 self.status.is_closed?
659 self.status.is_closed?
659 end
660 end
660
661
661 # Return true if the issue is being reopened
662 # Return true if the issue is being reopened
662 def reopened?
663 def reopened?
663 if !new_record? && status_id_changed?
664 if !new_record? && status_id_changed?
664 status_was = IssueStatus.find_by_id(status_id_was)
665 status_was = IssueStatus.find_by_id(status_id_was)
665 status_new = IssueStatus.find_by_id(status_id)
666 status_new = IssueStatus.find_by_id(status_id)
666 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
667 if status_was && status_new && status_was.is_closed? && !status_new.is_closed?
667 return true
668 return true
668 end
669 end
669 end
670 end
670 false
671 false
671 end
672 end
672
673
673 # Return true if the issue is being closed
674 # Return true if the issue is being closed
674 def closing?
675 def closing?
675 if !new_record? && status_id_changed?
676 if !new_record? && status_id_changed?
676 if status_was && status && !status_was.is_closed? && status.is_closed?
677 if status_was && status && !status_was.is_closed? && status.is_closed?
677 return true
678 return true
678 end
679 end
679 end
680 end
680 false
681 false
681 end
682 end
682
683
683 # Returns true if the issue is overdue
684 # Returns true if the issue is overdue
684 def overdue?
685 def overdue?
685 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
686 !due_date.nil? && (due_date < Date.today) && !status.is_closed?
686 end
687 end
687
688
688 # Is the amount of work done less than it should for the due date
689 # Is the amount of work done less than it should for the due date
689 def behind_schedule?
690 def behind_schedule?
690 return false if start_date.nil? || due_date.nil?
691 return false if start_date.nil? || due_date.nil?
691 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
692 done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
692 return done_date <= Date.today
693 return done_date <= Date.today
693 end
694 end
694
695
695 # Does this issue have children?
696 # Does this issue have children?
696 def children?
697 def children?
697 !leaf?
698 !leaf?
698 end
699 end
699
700
700 # Users the issue can be assigned to
701 # Users the issue can be assigned to
701 def assignable_users
702 def assignable_users
702 users = project.assignable_users
703 users = project.assignable_users
703 users << author if author
704 users << author if author
704 users << assigned_to if assigned_to
705 users << assigned_to if assigned_to
705 users.uniq.sort
706 users.uniq.sort
706 end
707 end
707
708
708 # Versions that the issue can be assigned to
709 # Versions that the issue can be assigned to
709 def assignable_versions
710 def assignable_versions
710 return @assignable_versions if @assignable_versions
711 return @assignable_versions if @assignable_versions
711
712
712 versions = project.shared_versions.open.all
713 versions = project.shared_versions.open.all
713 if fixed_version
714 if fixed_version
714 if fixed_version_id_changed?
715 if fixed_version_id_changed?
715 # nothing to do
716 # nothing to do
716 elsif project_id_changed?
717 elsif project_id_changed?
717 if project.shared_versions.include?(fixed_version)
718 if project.shared_versions.include?(fixed_version)
718 versions << fixed_version
719 versions << fixed_version
719 end
720 end
720 else
721 else
721 versions << fixed_version
722 versions << fixed_version
722 end
723 end
723 end
724 end
724 @assignable_versions = versions.uniq.sort
725 @assignable_versions = versions.uniq.sort
725 end
726 end
726
727
727 # Returns true if this issue is blocked by another issue that is still open
728 # Returns true if this issue is blocked by another issue that is still open
728 def blocked?
729 def blocked?
729 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
730 !relations_to.detect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed?}.nil?
730 end
731 end
731
732
732 # Returns an array of statuses that user is able to apply
733 # Returns an array of statuses that user is able to apply
733 def new_statuses_allowed_to(user=User.current, include_default=false)
734 def new_statuses_allowed_to(user=User.current, include_default=false)
734 if new_record? && @copied_from
735 if new_record? && @copied_from
735 [IssueStatus.default, @copied_from.status].compact.uniq.sort
736 [IssueStatus.default, @copied_from.status].compact.uniq.sort
736 else
737 else
737 initial_status = nil
738 initial_status = nil
738 if new_record?
739 if new_record?
739 initial_status = IssueStatus.default
740 initial_status = IssueStatus.default
740 elsif status_id_was
741 elsif status_id_was
741 initial_status = IssueStatus.find_by_id(status_id_was)
742 initial_status = IssueStatus.find_by_id(status_id_was)
742 end
743 end
743 initial_status ||= status
744 initial_status ||= status
744
745
745 statuses = initial_status.find_new_statuses_allowed_to(
746 statuses = initial_status.find_new_statuses_allowed_to(
746 user.admin ? Role.all : user.roles_for_project(project),
747 user.admin ? Role.all : user.roles_for_project(project),
747 tracker,
748 tracker,
748 author == user,
749 author == user,
749 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
750 assigned_to_id_changed? ? assigned_to_id_was == user.id : assigned_to_id == user.id
750 )
751 )
751 statuses << initial_status unless statuses.empty?
752 statuses << initial_status unless statuses.empty?
752 statuses << IssueStatus.default if include_default
753 statuses << IssueStatus.default if include_default
753 statuses = statuses.compact.uniq.sort
754 statuses = statuses.compact.uniq.sort
754 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
755 blocked? ? statuses.reject {|s| s.is_closed?} : statuses
755 end
756 end
756 end
757 end
757
758
758 def assigned_to_was
759 def assigned_to_was
759 if assigned_to_id_changed? && assigned_to_id_was.present?
760 if assigned_to_id_changed? && assigned_to_id_was.present?
760 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
761 @assigned_to_was ||= User.find_by_id(assigned_to_id_was)
761 end
762 end
762 end
763 end
763
764
764 # Returns the users that should be notified
765 # Returns the users that should be notified
765 def notified_users
766 def notified_users
766 notified = []
767 notified = []
767 # Author and assignee are always notified unless they have been
768 # Author and assignee are always notified unless they have been
768 # locked or don't want to be notified
769 # locked or don't want to be notified
769 notified << author if author
770 notified << author if author
770 if assigned_to
771 if assigned_to
771 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
772 notified += (assigned_to.is_a?(Group) ? assigned_to.users : [assigned_to])
772 end
773 end
773 if assigned_to_was
774 if assigned_to_was
774 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
775 notified += (assigned_to_was.is_a?(Group) ? assigned_to_was.users : [assigned_to_was])
775 end
776 end
776 notified = notified.select {|u| u.active? && u.notify_about?(self)}
777 notified = notified.select {|u| u.active? && u.notify_about?(self)}
777
778
778 notified += project.notified_users
779 notified += project.notified_users
779 notified.uniq!
780 notified.uniq!
780 # Remove users that can not view the issue
781 # Remove users that can not view the issue
781 notified.reject! {|user| !visible?(user)}
782 notified.reject! {|user| !visible?(user)}
782 notified
783 notified
783 end
784 end
784
785
785 # Returns the email addresses that should be notified
786 # Returns the email addresses that should be notified
786 def recipients
787 def recipients
787 notified_users.collect(&:mail)
788 notified_users.collect(&:mail)
788 end
789 end
789
790
790 # Returns the number of hours spent on this issue
791 # Returns the number of hours spent on this issue
791 def spent_hours
792 def spent_hours
792 @spent_hours ||= time_entries.sum(:hours) || 0
793 @spent_hours ||= time_entries.sum(:hours) || 0
793 end
794 end
794
795
795 # Returns the total number of hours spent on this issue and its descendants
796 # Returns the total number of hours spent on this issue and its descendants
796 #
797 #
797 # Example:
798 # Example:
798 # spent_hours => 0.0
799 # spent_hours => 0.0
799 # spent_hours => 50.2
800 # spent_hours => 50.2
800 def total_spent_hours
801 def total_spent_hours
801 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
802 @total_spent_hours ||= self_and_descendants.sum("#{TimeEntry.table_name}.hours",
802 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
803 :joins => "LEFT JOIN #{TimeEntry.table_name} ON #{TimeEntry.table_name}.issue_id = #{Issue.table_name}.id").to_f || 0.0
803 end
804 end
804
805
805 def relations
806 def relations
806 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
807 @relations ||= IssueRelation::Relations.new(self, (relations_from + relations_to).sort)
807 end
808 end
808
809
809 # Preloads relations for a collection of issues
810 # Preloads relations for a collection of issues
810 def self.load_relations(issues)
811 def self.load_relations(issues)
811 if issues.any?
812 if issues.any?
812 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
813 relations = IssueRelation.all(:conditions => ["issue_from_id IN (:ids) OR issue_to_id IN (:ids)", {:ids => issues.map(&:id)}])
813 issues.each do |issue|
814 issues.each do |issue|
814 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
815 issue.instance_variable_set "@relations", relations.select {|r| r.issue_from_id == issue.id || r.issue_to_id == issue.id}
815 end
816 end
816 end
817 end
817 end
818 end
818
819
819 # Preloads visible spent time for a collection of issues
820 # Preloads visible spent time for a collection of issues
820 def self.load_visible_spent_hours(issues, user=User.current)
821 def self.load_visible_spent_hours(issues, user=User.current)
821 if issues.any?
822 if issues.any?
822 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
823 hours_by_issue_id = TimeEntry.visible(user).sum(:hours, :group => :issue_id)
823 issues.each do |issue|
824 issues.each do |issue|
824 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
825 issue.instance_variable_set "@spent_hours", (hours_by_issue_id[issue.id] || 0)
825 end
826 end
826 end
827 end
827 end
828 end
828
829
829 # Preloads visible relations for a collection of issues
830 # Preloads visible relations for a collection of issues
830 def self.load_visible_relations(issues, user=User.current)
831 def self.load_visible_relations(issues, user=User.current)
831 if issues.any?
832 if issues.any?
832 issue_ids = issues.map(&:id)
833 issue_ids = issues.map(&:id)
833 # Relations with issue_from in given issues and visible issue_to
834 # Relations with issue_from in given issues and visible issue_to
834 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
835 relations_from = IssueRelation.includes(:issue_to => [:status, :project]).where(visible_condition(user)).where(:issue_from_id => issue_ids).all
835 # Relations with issue_to in given issues and visible issue_from
836 # Relations with issue_to in given issues and visible issue_from
836 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
837 relations_to = IssueRelation.includes(:issue_from => [:status, :project]).where(visible_condition(user)).where(:issue_to_id => issue_ids).all
837
838
838 issues.each do |issue|
839 issues.each do |issue|
839 relations =
840 relations =
840 relations_from.select {|relation| relation.issue_from_id == issue.id} +
841 relations_from.select {|relation| relation.issue_from_id == issue.id} +
841 relations_to.select {|relation| relation.issue_to_id == issue.id}
842 relations_to.select {|relation| relation.issue_to_id == issue.id}
842
843
843 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
844 issue.instance_variable_set "@relations", IssueRelation::Relations.new(issue, relations.sort)
844 end
845 end
845 end
846 end
846 end
847 end
847
848
848 # Finds an issue relation given its id.
849 # Finds an issue relation given its id.
849 def find_relation(relation_id)
850 def find_relation(relation_id)
850 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
851 IssueRelation.find(relation_id, :conditions => ["issue_to_id = ? OR issue_from_id = ?", id, id])
851 end
852 end
852
853
853 def all_dependent_issues(except=[])
854 def all_dependent_issues(except=[])
854 except << self
855 except << self
855 dependencies = []
856 dependencies = []
856 relations_from.each do |relation|
857 relations_from.each do |relation|
857 if relation.issue_to && !except.include?(relation.issue_to)
858 if relation.issue_to && !except.include?(relation.issue_to)
858 dependencies << relation.issue_to
859 dependencies << relation.issue_to
859 dependencies += relation.issue_to.all_dependent_issues(except)
860 dependencies += relation.issue_to.all_dependent_issues(except)
860 end
861 end
861 end
862 end
862 dependencies
863 dependencies
863 end
864 end
864
865
865 # Returns an array of issues that duplicate this one
866 # Returns an array of issues that duplicate this one
866 def duplicates
867 def duplicates
867 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
868 relations_to.select {|r| r.relation_type == IssueRelation::TYPE_DUPLICATES}.collect {|r| r.issue_from}
868 end
869 end
869
870
870 # Returns the due date or the target due date if any
871 # Returns the due date or the target due date if any
871 # Used on gantt chart
872 # Used on gantt chart
872 def due_before
873 def due_before
873 due_date || (fixed_version ? fixed_version.effective_date : nil)
874 due_date || (fixed_version ? fixed_version.effective_date : nil)
874 end
875 end
875
876
876 # Returns the time scheduled for this issue.
877 # Returns the time scheduled for this issue.
877 #
878 #
878 # Example:
879 # Example:
879 # Start Date: 2/26/09, End Date: 3/04/09
880 # Start Date: 2/26/09, End Date: 3/04/09
880 # duration => 6
881 # duration => 6
881 def duration
882 def duration
882 (start_date && due_date) ? due_date - start_date : 0
883 (start_date && due_date) ? due_date - start_date : 0
883 end
884 end
884
885
885 # Returns the duration in working days
886 # Returns the duration in working days
886 def working_duration
887 def working_duration
887 (start_date && due_date) ? working_days(start_date, due_date) : 0
888 (start_date && due_date) ? working_days(start_date, due_date) : 0
888 end
889 end
889
890
890 def soonest_start(reload=false)
891 def soonest_start(reload=false)
891 @soonest_start = nil if reload
892 @soonest_start = nil if reload
892 @soonest_start ||= (
893 @soonest_start ||= (
893 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
894 relations_to(reload).collect{|relation| relation.successor_soonest_start} +
894 ancestors.collect(&:soonest_start)
895 ancestors.collect(&:soonest_start)
895 ).compact.max
896 ).compact.max
896 end
897 end
897
898
898 # Sets start_date on the given date or the next working day
899 # Sets start_date on the given date or the next working day
899 # and changes due_date to keep the same working duration.
900 # and changes due_date to keep the same working duration.
900 def reschedule_on(date)
901 def reschedule_on(date)
901 wd = working_duration
902 wd = working_duration
902 date = next_working_date(date)
903 date = next_working_date(date)
903 self.start_date = date
904 self.start_date = date
904 self.due_date = add_working_days(date, wd)
905 self.due_date = add_working_days(date, wd)
905 end
906 end
906
907
907 # Reschedules the issue on the given date or the next working day and saves the record.
908 # Reschedules the issue on the given date or the next working day and saves the record.
908 # If the issue is a parent task, this is done by rescheduling its subtasks.
909 # If the issue is a parent task, this is done by rescheduling its subtasks.
909 def reschedule_on!(date)
910 def reschedule_on!(date)
910 return if date.nil?
911 return if date.nil?
911 if leaf?
912 if leaf?
912 if start_date.nil? || start_date != date
913 if start_date.nil? || start_date != date
913 if start_date && start_date > date
914 if start_date && start_date > date
914 # Issue can not be moved earlier than its soonest start date
915 # Issue can not be moved earlier than its soonest start date
915 date = [soonest_start(true), date].compact.max
916 date = [soonest_start(true), date].compact.max
916 end
917 end
917 reschedule_on(date)
918 reschedule_on(date)
918 begin
919 begin
919 save
920 save
920 rescue ActiveRecord::StaleObjectError
921 rescue ActiveRecord::StaleObjectError
921 reload
922 reload
922 reschedule_on(date)
923 reschedule_on(date)
923 save
924 save
924 end
925 end
925 end
926 end
926 else
927 else
927 leaves.each do |leaf|
928 leaves.each do |leaf|
928 if leaf.start_date
929 if leaf.start_date
929 # Only move subtask if it starts at the same date as the parent
930 # Only move subtask if it starts at the same date as the parent
930 # or if it starts before the given date
931 # or if it starts before the given date
931 if start_date == leaf.start_date || date > leaf.start_date
932 if start_date == leaf.start_date || date > leaf.start_date
932 leaf.reschedule_on!(date)
933 leaf.reschedule_on!(date)
933 end
934 end
934 else
935 else
935 leaf.reschedule_on!(date)
936 leaf.reschedule_on!(date)
936 end
937 end
937 end
938 end
938 end
939 end
939 end
940 end
940
941
941 def <=>(issue)
942 def <=>(issue)
942 if issue.nil?
943 if issue.nil?
943 -1
944 -1
944 elsif root_id != issue.root_id
945 elsif root_id != issue.root_id
945 (root_id || 0) <=> (issue.root_id || 0)
946 (root_id || 0) <=> (issue.root_id || 0)
946 else
947 else
947 (lft || 0) <=> (issue.lft || 0)
948 (lft || 0) <=> (issue.lft || 0)
948 end
949 end
949 end
950 end
950
951
951 def to_s
952 def to_s
952 "#{tracker} ##{id}: #{subject}"
953 "#{tracker} ##{id}: #{subject}"
953 end
954 end
954
955
955 # Returns a string of css classes that apply to the issue
956 # Returns a string of css classes that apply to the issue
956 def css_classes
957 def css_classes
957 s = "issue status-#{status_id} #{priority.try(:css_classes)}"
958 s = "issue status-#{status_id} #{priority.try(:css_classes)}"
958 s << ' closed' if closed?
959 s << ' closed' if closed?
959 s << ' overdue' if overdue?
960 s << ' overdue' if overdue?
960 s << ' child' if child?
961 s << ' child' if child?
961 s << ' parent' unless leaf?
962 s << ' parent' unless leaf?
962 s << ' private' if is_private?
963 s << ' private' if is_private?
963 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
964 s << ' created-by-me' if User.current.logged? && author_id == User.current.id
964 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
965 s << ' assigned-to-me' if User.current.logged? && assigned_to_id == User.current.id
965 s
966 s
966 end
967 end
967
968
968 # Saves an issue and a time_entry from the parameters
969 # Saves an issue and a time_entry from the parameters
969 def save_issue_with_child_records(params, existing_time_entry=nil)
970 def save_issue_with_child_records(params, existing_time_entry=nil)
970 Issue.transaction do
971 Issue.transaction do
971 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
972 if params[:time_entry] && (params[:time_entry][:hours].present? || params[:time_entry][:comments].present?) && User.current.allowed_to?(:log_time, project)
972 @time_entry = existing_time_entry || TimeEntry.new
973 @time_entry = existing_time_entry || TimeEntry.new
973 @time_entry.project = project
974 @time_entry.project = project
974 @time_entry.issue = self
975 @time_entry.issue = self
975 @time_entry.user = User.current
976 @time_entry.user = User.current
976 @time_entry.spent_on = User.current.today
977 @time_entry.spent_on = User.current.today
977 @time_entry.attributes = params[:time_entry]
978 @time_entry.attributes = params[:time_entry]
978 self.time_entries << @time_entry
979 self.time_entries << @time_entry
979 end
980 end
980
981
981 # TODO: Rename hook
982 # TODO: Rename hook
982 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
983 Redmine::Hook.call_hook(:controller_issues_edit_before_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
983 if save
984 if save
984 # TODO: Rename hook
985 # TODO: Rename hook
985 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
986 Redmine::Hook.call_hook(:controller_issues_edit_after_save, { :params => params, :issue => self, :time_entry => @time_entry, :journal => @current_journal})
986 else
987 else
987 raise ActiveRecord::Rollback
988 raise ActiveRecord::Rollback
988 end
989 end
989 end
990 end
990 end
991 end
991
992
992 # Unassigns issues from +version+ if it's no longer shared with issue's project
993 # Unassigns issues from +version+ if it's no longer shared with issue's project
993 def self.update_versions_from_sharing_change(version)
994 def self.update_versions_from_sharing_change(version)
994 # Update issues assigned to the version
995 # Update issues assigned to the version
995 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
996 update_versions(["#{Issue.table_name}.fixed_version_id = ?", version.id])
996 end
997 end
997
998
998 # Unassigns issues from versions that are no longer shared
999 # Unassigns issues from versions that are no longer shared
999 # after +project+ was moved
1000 # after +project+ was moved
1000 def self.update_versions_from_hierarchy_change(project)
1001 def self.update_versions_from_hierarchy_change(project)
1001 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1002 moved_project_ids = project.self_and_descendants.reload.collect(&:id)
1002 # Update issues of the moved projects and issues assigned to a version of a moved project
1003 # Update issues of the moved projects and issues assigned to a version of a moved project
1003 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1004 Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
1004 end
1005 end
1005
1006
1006 def parent_issue_id=(arg)
1007 def parent_issue_id=(arg)
1007 s = arg.to_s.strip.presence
1008 s = arg.to_s.strip.presence
1008 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1009 if s && (m = s.match(%r{\A#?(\d+)\z})) && (@parent_issue = Issue.find_by_id(m[1]))
1009 @parent_issue.id
1010 @parent_issue.id
1010 else
1011 else
1011 @parent_issue = nil
1012 @parent_issue = nil
1012 @invalid_parent_issue_id = arg
1013 @invalid_parent_issue_id = arg
1013 end
1014 end
1014 end
1015 end
1015
1016
1016 def parent_issue_id
1017 def parent_issue_id
1017 if @invalid_parent_issue_id
1018 if @invalid_parent_issue_id
1018 @invalid_parent_issue_id
1019 @invalid_parent_issue_id
1019 elsif instance_variable_defined? :@parent_issue
1020 elsif instance_variable_defined? :@parent_issue
1020 @parent_issue.nil? ? nil : @parent_issue.id
1021 @parent_issue.nil? ? nil : @parent_issue.id
1021 else
1022 else
1022 parent_id
1023 parent_id
1023 end
1024 end
1024 end
1025 end
1025
1026
1026 # Returns true if issue's project is a valid
1027 # Returns true if issue's project is a valid
1027 # parent issue project
1028 # parent issue project
1028 def valid_parent_project?(issue=parent)
1029 def valid_parent_project?(issue=parent)
1029 return true if issue.nil? || issue.project_id == project_id
1030 return true if issue.nil? || issue.project_id == project_id
1030
1031
1031 case Setting.cross_project_subtasks
1032 case Setting.cross_project_subtasks
1032 when 'system'
1033 when 'system'
1033 true
1034 true
1034 when 'tree'
1035 when 'tree'
1035 issue.project.root == project.root
1036 issue.project.root == project.root
1036 when 'hierarchy'
1037 when 'hierarchy'
1037 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1038 issue.project.is_or_is_ancestor_of?(project) || issue.project.is_descendant_of?(project)
1038 when 'descendants'
1039 when 'descendants'
1039 issue.project.is_or_is_ancestor_of?(project)
1040 issue.project.is_or_is_ancestor_of?(project)
1040 else
1041 else
1041 false
1042 false
1042 end
1043 end
1043 end
1044 end
1044
1045
1045 # Extracted from the ReportsController.
1046 # Extracted from the ReportsController.
1046 def self.by_tracker(project)
1047 def self.by_tracker(project)
1047 count_and_group_by(:project => project,
1048 count_and_group_by(:project => project,
1048 :field => 'tracker_id',
1049 :field => 'tracker_id',
1049 :joins => Tracker.table_name)
1050 :joins => Tracker.table_name)
1050 end
1051 end
1051
1052
1052 def self.by_version(project)
1053 def self.by_version(project)
1053 count_and_group_by(:project => project,
1054 count_and_group_by(:project => project,
1054 :field => 'fixed_version_id',
1055 :field => 'fixed_version_id',
1055 :joins => Version.table_name)
1056 :joins => Version.table_name)
1056 end
1057 end
1057
1058
1058 def self.by_priority(project)
1059 def self.by_priority(project)
1059 count_and_group_by(:project => project,
1060 count_and_group_by(:project => project,
1060 :field => 'priority_id',
1061 :field => 'priority_id',
1061 :joins => IssuePriority.table_name)
1062 :joins => IssuePriority.table_name)
1062 end
1063 end
1063
1064
1064 def self.by_category(project)
1065 def self.by_category(project)
1065 count_and_group_by(:project => project,
1066 count_and_group_by(:project => project,
1066 :field => 'category_id',
1067 :field => 'category_id',
1067 :joins => IssueCategory.table_name)
1068 :joins => IssueCategory.table_name)
1068 end
1069 end
1069
1070
1070 def self.by_assigned_to(project)
1071 def self.by_assigned_to(project)
1071 count_and_group_by(:project => project,
1072 count_and_group_by(:project => project,
1072 :field => 'assigned_to_id',
1073 :field => 'assigned_to_id',
1073 :joins => User.table_name)
1074 :joins => User.table_name)
1074 end
1075 end
1075
1076
1076 def self.by_author(project)
1077 def self.by_author(project)
1077 count_and_group_by(:project => project,
1078 count_and_group_by(:project => project,
1078 :field => 'author_id',
1079 :field => 'author_id',
1079 :joins => User.table_name)
1080 :joins => User.table_name)
1080 end
1081 end
1081
1082
1082 def self.by_subproject(project)
1083 def self.by_subproject(project)
1083 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1084 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1084 s.is_closed as closed,
1085 s.is_closed as closed,
1085 #{Issue.table_name}.project_id as project_id,
1086 #{Issue.table_name}.project_id as project_id,
1086 count(#{Issue.table_name}.id) as total
1087 count(#{Issue.table_name}.id) as total
1087 from
1088 from
1088 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1089 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s
1089 where
1090 where
1090 #{Issue.table_name}.status_id=s.id
1091 #{Issue.table_name}.status_id=s.id
1091 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1092 and #{Issue.table_name}.project_id = #{Project.table_name}.id
1092 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1093 and #{visible_condition(User.current, :project => project, :with_subprojects => true)}
1093 and #{Issue.table_name}.project_id <> #{project.id}
1094 and #{Issue.table_name}.project_id <> #{project.id}
1094 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1095 group by s.id, s.is_closed, #{Issue.table_name}.project_id") if project.descendants.active.any?
1095 end
1096 end
1096 # End ReportsController extraction
1097 # End ReportsController extraction
1097
1098
1098 # Returns an array of projects that user can assign the issue to
1099 # Returns an array of projects that user can assign the issue to
1099 def allowed_target_projects(user=User.current)
1100 def allowed_target_projects(user=User.current)
1100 if new_record?
1101 if new_record?
1101 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1102 Project.all(:conditions => Project.allowed_to_condition(user, :add_issues))
1102 else
1103 else
1103 self.class.allowed_target_projects_on_move(user)
1104 self.class.allowed_target_projects_on_move(user)
1104 end
1105 end
1105 end
1106 end
1106
1107
1107 # Returns an array of projects that user can move issues to
1108 # Returns an array of projects that user can move issues to
1108 def self.allowed_target_projects_on_move(user=User.current)
1109 def self.allowed_target_projects_on_move(user=User.current)
1109 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1110 Project.all(:conditions => Project.allowed_to_condition(user, :move_issues))
1110 end
1111 end
1111
1112
1112 private
1113 private
1113
1114
1114 def after_project_change
1115 def after_project_change
1115 # Update project_id on related time entries
1116 # Update project_id on related time entries
1116 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1117 TimeEntry.update_all(["project_id = ?", project_id], {:issue_id => id})
1117
1118
1118 # Delete issue relations
1119 # Delete issue relations
1119 unless Setting.cross_project_issue_relations?
1120 unless Setting.cross_project_issue_relations?
1120 relations_from.clear
1121 relations_from.clear
1121 relations_to.clear
1122 relations_to.clear
1122 end
1123 end
1123
1124
1124 # Move subtasks that were in the same project
1125 # Move subtasks that were in the same project
1125 children.each do |child|
1126 children.each do |child|
1126 next unless child.project_id == project_id_was
1127 next unless child.project_id == project_id_was
1127 # Change project and keep project
1128 # Change project and keep project
1128 child.send :project=, project, true
1129 child.send :project=, project, true
1129 unless child.save
1130 unless child.save
1130 raise ActiveRecord::Rollback
1131 raise ActiveRecord::Rollback
1131 end
1132 end
1132 end
1133 end
1133 end
1134 end
1134
1135
1135 # Callback for after the creation of an issue by copy
1136 # Callback for after the creation of an issue by copy
1136 # * adds a "copied to" relation with the copied issue
1137 # * adds a "copied to" relation with the copied issue
1137 # * copies subtasks from the copied issue
1138 # * copies subtasks from the copied issue
1138 def after_create_from_copy
1139 def after_create_from_copy
1139 return unless copy? && !@after_create_from_copy_handled
1140 return unless copy? && !@after_create_from_copy_handled
1140
1141
1141 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1142 if (@copied_from.project_id == project_id || Setting.cross_project_issue_relations?) && @copy_options[:link] != false
1142 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1143 relation = IssueRelation.new(:issue_from => @copied_from, :issue_to => self, :relation_type => IssueRelation::TYPE_COPIED_TO)
1143 unless relation.save
1144 unless relation.save
1144 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1145 logger.error "Could not create relation while copying ##{@copied_from.id} to ##{id} due to validation errors: #{relation.errors.full_messages.join(', ')}" if logger
1145 end
1146 end
1146 end
1147 end
1147
1148
1148 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1149 unless @copied_from.leaf? || @copy_options[:subtasks] == false
1149 @copied_from.children.each do |child|
1150 @copied_from.children.each do |child|
1150 unless child.visible?
1151 unless child.visible?
1151 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1152 # Do not copy subtasks that are not visible to avoid potential disclosure of private data
1152 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1153 logger.error "Subtask ##{child.id} was not copied during ##{@copied_from.id} copy because it is not visible to the current user" if logger
1153 next
1154 next
1154 end
1155 end
1155 copy = Issue.new.copy_from(child, @copy_options)
1156 copy = Issue.new.copy_from(child, @copy_options)
1156 copy.author = author
1157 copy.author = author
1157 copy.project = project
1158 copy.project = project
1158 copy.parent_issue_id = id
1159 copy.parent_issue_id = id
1159 # Children subtasks are copied recursively
1160 # Children subtasks are copied recursively
1160 unless copy.save
1161 unless copy.save
1161 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
1162 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
1162 end
1163 end
1163 end
1164 end
1164 end
1165 end
1165 @after_create_from_copy_handled = true
1166 @after_create_from_copy_handled = true
1166 end
1167 end
1167
1168
1168 def update_nested_set_attributes
1169 def update_nested_set_attributes
1169 if root_id.nil?
1170 if root_id.nil?
1170 # issue was just created
1171 # issue was just created
1171 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1172 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id)
1172 set_default_left_and_right
1173 set_default_left_and_right
1173 Issue.update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt], ["id = ?", id])
1174 Issue.update_all(["root_id = ?, lft = ?, rgt = ?", root_id, lft, rgt], ["id = ?", id])
1174 if @parent_issue
1175 if @parent_issue
1175 move_to_child_of(@parent_issue)
1176 move_to_child_of(@parent_issue)
1176 end
1177 end
1177 elsif parent_issue_id != parent_id
1178 elsif parent_issue_id != parent_id
1178 former_parent_id = parent_id
1179 former_parent_id = parent_id
1179 # moving an existing issue
1180 # moving an existing issue
1180 if @parent_issue && @parent_issue.root_id == root_id
1181 if @parent_issue && @parent_issue.root_id == root_id
1181 # inside the same tree
1182 # inside the same tree
1182 move_to_child_of(@parent_issue)
1183 move_to_child_of(@parent_issue)
1183 else
1184 else
1184 # to another tree
1185 # to another tree
1185 unless root?
1186 unless root?
1186 move_to_right_of(root)
1187 move_to_right_of(root)
1187 end
1188 end
1188 old_root_id = root_id
1189 old_root_id = root_id
1189 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1190 self.root_id = (@parent_issue.nil? ? id : @parent_issue.root_id )
1190 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1191 target_maxright = nested_set_scope.maximum(right_column_name) || 0
1191 offset = target_maxright + 1 - lft
1192 offset = target_maxright + 1 - lft
1192 Issue.update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset],
1193 Issue.update_all(["root_id = ?, lft = lft + ?, rgt = rgt + ?", root_id, offset, offset],
1193 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1194 ["root_id = ? AND lft >= ? AND rgt <= ? ", old_root_id, lft, rgt])
1194 self[left_column_name] = lft + offset
1195 self[left_column_name] = lft + offset
1195 self[right_column_name] = rgt + offset
1196 self[right_column_name] = rgt + offset
1196 if @parent_issue
1197 if @parent_issue
1197 move_to_child_of(@parent_issue)
1198 move_to_child_of(@parent_issue)
1198 end
1199 end
1199 end
1200 end
1200 # delete invalid relations of all descendants
1201 # delete invalid relations of all descendants
1201 self_and_descendants.each do |issue|
1202 self_and_descendants.each do |issue|
1202 issue.relations.each do |relation|
1203 issue.relations.each do |relation|
1203 relation.destroy unless relation.valid?
1204 relation.destroy unless relation.valid?
1204 end
1205 end
1205 end
1206 end
1206 # update former parent
1207 # update former parent
1207 recalculate_attributes_for(former_parent_id) if former_parent_id
1208 recalculate_attributes_for(former_parent_id) if former_parent_id
1208 end
1209 end
1209 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1210 remove_instance_variable(:@parent_issue) if instance_variable_defined?(:@parent_issue)
1210 end
1211 end
1211
1212
1212 def update_parent_attributes
1213 def update_parent_attributes
1213 recalculate_attributes_for(parent_id) if parent_id
1214 recalculate_attributes_for(parent_id) if parent_id
1214 end
1215 end
1215
1216
1216 def recalculate_attributes_for(issue_id)
1217 def recalculate_attributes_for(issue_id)
1217 if issue_id && p = Issue.find_by_id(issue_id)
1218 if issue_id && p = Issue.find_by_id(issue_id)
1218 # priority = highest priority of children
1219 # priority = highest priority of children
1219 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1220 if priority_position = p.children.maximum("#{IssuePriority.table_name}.position", :joins => :priority)
1220 p.priority = IssuePriority.find_by_position(priority_position)
1221 p.priority = IssuePriority.find_by_position(priority_position)
1221 end
1222 end
1222
1223
1223 # start/due dates = lowest/highest dates of children
1224 # start/due dates = lowest/highest dates of children
1224 p.start_date = p.children.minimum(:start_date)
1225 p.start_date = p.children.minimum(:start_date)
1225 p.due_date = p.children.maximum(:due_date)
1226 p.due_date = p.children.maximum(:due_date)
1226 if p.start_date && p.due_date && p.due_date < p.start_date
1227 if p.start_date && p.due_date && p.due_date < p.start_date
1227 p.start_date, p.due_date = p.due_date, p.start_date
1228 p.start_date, p.due_date = p.due_date, p.start_date
1228 end
1229 end
1229
1230
1230 # done ratio = weighted average ratio of leaves
1231 # done ratio = weighted average ratio of leaves
1231 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1232 unless Issue.use_status_for_done_ratio? && p.status && p.status.default_done_ratio
1232 leaves_count = p.leaves.count
1233 leaves_count = p.leaves.count
1233 if leaves_count > 0
1234 if leaves_count > 0
1234 average = p.leaves.average(:estimated_hours).to_f
1235 average = p.leaves.average(:estimated_hours).to_f
1235 if average == 0
1236 if average == 0
1236 average = 1
1237 average = 1
1237 end
1238 end
1238 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
1239 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
1239 progress = done / (average * leaves_count)
1240 progress = done / (average * leaves_count)
1240 p.done_ratio = progress.round
1241 p.done_ratio = progress.round
1241 end
1242 end
1242 end
1243 end
1243
1244
1244 # estimate = sum of leaves estimates
1245 # estimate = sum of leaves estimates
1245 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1246 p.estimated_hours = p.leaves.sum(:estimated_hours).to_f
1246 p.estimated_hours = nil if p.estimated_hours == 0.0
1247 p.estimated_hours = nil if p.estimated_hours == 0.0
1247
1248
1248 # ancestors will be recursively updated
1249 # ancestors will be recursively updated
1249 p.save(:validate => false)
1250 p.save(:validate => false)
1250 end
1251 end
1251 end
1252 end
1252
1253
1253 # Update issues so their versions are not pointing to a
1254 # Update issues so their versions are not pointing to a
1254 # fixed_version that is not shared with the issue's project
1255 # fixed_version that is not shared with the issue's project
1255 def self.update_versions(conditions=nil)
1256 def self.update_versions(conditions=nil)
1256 # Only need to update issues with a fixed_version from
1257 # Only need to update issues with a fixed_version from
1257 # a different project and that is not systemwide shared
1258 # a different project and that is not systemwide shared
1258 Issue.scoped(:conditions => conditions).all(
1259 Issue.scoped(:conditions => conditions).all(
1259 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1260 :conditions => "#{Issue.table_name}.fixed_version_id IS NOT NULL" +
1260 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1261 " AND #{Issue.table_name}.project_id <> #{Version.table_name}.project_id" +
1261 " AND #{Version.table_name}.sharing <> 'system'",
1262 " AND #{Version.table_name}.sharing <> 'system'",
1262 :include => [:project, :fixed_version]
1263 :include => [:project, :fixed_version]
1263 ).each do |issue|
1264 ).each do |issue|
1264 next if issue.project.nil? || issue.fixed_version.nil?
1265 next if issue.project.nil? || issue.fixed_version.nil?
1265 unless issue.project.shared_versions.include?(issue.fixed_version)
1266 unless issue.project.shared_versions.include?(issue.fixed_version)
1266 issue.init_journal(User.current)
1267 issue.init_journal(User.current)
1267 issue.fixed_version = nil
1268 issue.fixed_version = nil
1268 issue.save
1269 issue.save
1269 end
1270 end
1270 end
1271 end
1271 end
1272 end
1272
1273
1273 # Callback on file attachment
1274 # Callback on file attachment
1274 def attachment_added(obj)
1275 def attachment_added(obj)
1275 if @current_journal && !obj.new_record?
1276 if @current_journal && !obj.new_record?
1276 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1277 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :value => obj.filename)
1277 end
1278 end
1278 end
1279 end
1279
1280
1280 # Callback on attachment deletion
1281 # Callback on attachment deletion
1281 def attachment_removed(obj)
1282 def attachment_removed(obj)
1282 if @current_journal && !obj.new_record?
1283 if @current_journal && !obj.new_record?
1283 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1284 @current_journal.details << JournalDetail.new(:property => 'attachment', :prop_key => obj.id, :old_value => obj.filename)
1284 @current_journal.save
1285 @current_journal.save
1285 end
1286 end
1286 end
1287 end
1287
1288
1288 # Default assignment based on category
1289 # Default assignment based on category
1289 def default_assign
1290 def default_assign
1290 if assigned_to.nil? && category && category.assigned_to
1291 if assigned_to.nil? && category && category.assigned_to
1291 self.assigned_to = category.assigned_to
1292 self.assigned_to = category.assigned_to
1292 end
1293 end
1293 end
1294 end
1294
1295
1295 # Updates start/due dates of following issues
1296 # Updates start/due dates of following issues
1296 def reschedule_following_issues
1297 def reschedule_following_issues
1297 if start_date_changed? || due_date_changed?
1298 if start_date_changed? || due_date_changed?
1298 relations_from.each do |relation|
1299 relations_from.each do |relation|
1299 relation.set_issue_to_dates
1300 relation.set_issue_to_dates
1300 end
1301 end
1301 end
1302 end
1302 end
1303 end
1303
1304
1304 # Closes duplicates if the issue is being closed
1305 # Closes duplicates if the issue is being closed
1305 def close_duplicates
1306 def close_duplicates
1306 if closing?
1307 if closing?
1307 duplicates.each do |duplicate|
1308 duplicates.each do |duplicate|
1308 # Reload is need in case the duplicate was updated by a previous duplicate
1309 # Reload is need in case the duplicate was updated by a previous duplicate
1309 duplicate.reload
1310 duplicate.reload
1310 # Don't re-close it if it's already closed
1311 # Don't re-close it if it's already closed
1311 next if duplicate.closed?
1312 next if duplicate.closed?
1312 # Same user and notes
1313 # Same user and notes
1313 if @current_journal
1314 if @current_journal
1314 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1315 duplicate.init_journal(@current_journal.user, @current_journal.notes)
1315 end
1316 end
1316 duplicate.update_attribute :status, self.status
1317 duplicate.update_attribute :status, self.status
1317 end
1318 end
1318 end
1319 end
1319 end
1320 end
1320
1321
1321 # Make sure updated_on is updated when adding a note and set updated_on now
1322 # Make sure updated_on is updated when adding a note and set updated_on now
1322 # so we can set closed_on with the same value on closing
1323 # so we can set closed_on with the same value on closing
1323 def force_updated_on_change
1324 def force_updated_on_change
1324 if @current_journal || changed?
1325 if @current_journal || changed?
1325 self.updated_on = current_time_from_proper_timezone
1326 self.updated_on = current_time_from_proper_timezone
1326 if new_record?
1327 if new_record?
1327 self.created_on = updated_on
1328 self.created_on = updated_on
1328 end
1329 end
1329 end
1330 end
1330 end
1331 end
1331
1332
1332 # Callback for setting closed_on when the issue is closed.
1333 # Callback for setting closed_on when the issue is closed.
1333 # The closed_on attribute stores the time of the last closing
1334 # The closed_on attribute stores the time of the last closing
1334 # and is preserved when the issue is reopened.
1335 # and is preserved when the issue is reopened.
1335 def update_closed_on
1336 def update_closed_on
1336 if closing? || (new_record? && closed?)
1337 if closing? || (new_record? && closed?)
1337 self.closed_on = updated_on
1338 self.closed_on = updated_on
1338 end
1339 end
1339 end
1340 end
1340
1341
1341 # Saves the changes in a Journal
1342 # Saves the changes in a Journal
1342 # Called after_save
1343 # Called after_save
1343 def create_journal
1344 def create_journal
1344 if @current_journal
1345 if @current_journal
1345 # attributes changes
1346 # attributes changes
1346 if @attributes_before_change
1347 if @attributes_before_change
1347 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1348 (Issue.column_names - %w(id root_id lft rgt lock_version created_on updated_on closed_on)).each {|c|
1348 before = @attributes_before_change[c]
1349 before = @attributes_before_change[c]
1349 after = send(c)
1350 after = send(c)
1350 next if before == after || (before.blank? && after.blank?)
1351 next if before == after || (before.blank? && after.blank?)
1351 @current_journal.details << JournalDetail.new(:property => 'attr',
1352 @current_journal.details << JournalDetail.new(:property => 'attr',
1352 :prop_key => c,
1353 :prop_key => c,
1353 :old_value => before,
1354 :old_value => before,
1354 :value => after)
1355 :value => after)
1355 }
1356 }
1356 end
1357 end
1357 if @custom_values_before_change
1358 if @custom_values_before_change
1358 # custom fields changes
1359 # custom fields changes
1359 custom_field_values.each {|c|
1360 custom_field_values.each {|c|
1360 before = @custom_values_before_change[c.custom_field_id]
1361 before = @custom_values_before_change[c.custom_field_id]
1361 after = c.value
1362 after = c.value
1362 next if before == after || (before.blank? && after.blank?)
1363 next if before == after || (before.blank? && after.blank?)
1363
1364
1364 if before.is_a?(Array) || after.is_a?(Array)
1365 if before.is_a?(Array) || after.is_a?(Array)
1365 before = [before] unless before.is_a?(Array)
1366 before = [before] unless before.is_a?(Array)
1366 after = [after] unless after.is_a?(Array)
1367 after = [after] unless after.is_a?(Array)
1367
1368
1368 # values removed
1369 # values removed
1369 (before - after).reject(&:blank?).each do |value|
1370 (before - after).reject(&:blank?).each do |value|
1370 @current_journal.details << JournalDetail.new(:property => 'cf',
1371 @current_journal.details << JournalDetail.new(:property => 'cf',
1371 :prop_key => c.custom_field_id,
1372 :prop_key => c.custom_field_id,
1372 :old_value => value,
1373 :old_value => value,
1373 :value => nil)
1374 :value => nil)
1374 end
1375 end
1375 # values added
1376 # values added
1376 (after - before).reject(&:blank?).each do |value|
1377 (after - before).reject(&:blank?).each do |value|
1377 @current_journal.details << JournalDetail.new(:property => 'cf',
1378 @current_journal.details << JournalDetail.new(:property => 'cf',
1378 :prop_key => c.custom_field_id,
1379 :prop_key => c.custom_field_id,
1379 :old_value => nil,
1380 :old_value => nil,
1380 :value => value)
1381 :value => value)
1381 end
1382 end
1382 else
1383 else
1383 @current_journal.details << JournalDetail.new(:property => 'cf',
1384 @current_journal.details << JournalDetail.new(:property => 'cf',
1384 :prop_key => c.custom_field_id,
1385 :prop_key => c.custom_field_id,
1385 :old_value => before,
1386 :old_value => before,
1386 :value => after)
1387 :value => after)
1387 end
1388 end
1388 }
1389 }
1389 end
1390 end
1390 @current_journal.save
1391 @current_journal.save
1391 # reset current journal
1392 # reset current journal
1392 init_journal @current_journal.user, @current_journal.notes
1393 init_journal @current_journal.user, @current_journal.notes
1393 end
1394 end
1394 end
1395 end
1395
1396
1396 # Query generator for selecting groups of issue counts for a project
1397 # Query generator for selecting groups of issue counts for a project
1397 # based on specific criteria
1398 # based on specific criteria
1398 #
1399 #
1399 # Options
1400 # Options
1400 # * project - Project to search in.
1401 # * project - Project to search in.
1401 # * field - String. Issue field to key off of in the grouping.
1402 # * field - String. Issue field to key off of in the grouping.
1402 # * joins - String. The table name to join against.
1403 # * joins - String. The table name to join against.
1403 def self.count_and_group_by(options)
1404 def self.count_and_group_by(options)
1404 project = options.delete(:project)
1405 project = options.delete(:project)
1405 select_field = options.delete(:field)
1406 select_field = options.delete(:field)
1406 joins = options.delete(:joins)
1407 joins = options.delete(:joins)
1407
1408
1408 where = "#{Issue.table_name}.#{select_field}=j.id"
1409 where = "#{Issue.table_name}.#{select_field}=j.id"
1409
1410
1410 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1411 ActiveRecord::Base.connection.select_all("select s.id as status_id,
1411 s.is_closed as closed,
1412 s.is_closed as closed,
1412 j.id as #{select_field},
1413 j.id as #{select_field},
1413 count(#{Issue.table_name}.id) as total
1414 count(#{Issue.table_name}.id) as total
1414 from
1415 from
1415 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1416 #{Issue.table_name}, #{Project.table_name}, #{IssueStatus.table_name} s, #{joins} j
1416 where
1417 where
1417 #{Issue.table_name}.status_id=s.id
1418 #{Issue.table_name}.status_id=s.id
1418 and #{where}
1419 and #{where}
1419 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1420 and #{Issue.table_name}.project_id=#{Project.table_name}.id
1420 and #{visible_condition(User.current, :project => project)}
1421 and #{visible_condition(User.current, :project => project)}
1421 group by s.id, s.is_closed, j.id")
1422 group by s.id, s.is_closed, j.id")
1422 end
1423 end
1423 end
1424 end
@@ -1,1025 +1,1026
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_CLOSED = 5
23 STATUS_CLOSED = 5
24 STATUS_ARCHIVED = 9
24 STATUS_ARCHIVED = 9
25
25
26 # Maximum length for project identifiers
26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100
27 IDENTIFIER_MAX_LENGTH = 100
28
28
29 # Specific overidden Activities
29 # Specific overidden Activities
30 has_many :time_entry_activities
30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member'
32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member',
33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal,
34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 has_many :users, :through => :members
36 has_many :users, :through => :members
37 has_many :principals, :through => :member_principals, :source => :principal
37 has_many :principals, :through => :member_principals, :source => :principal
38
38
39 has_many :enabled_modules, :dependent => :delete_all
39 has_many :enabled_modules, :dependent => :delete_all
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :issue_changes, :through => :issues, :source => :journals
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 has_many :time_entries, :dependent => :delete_all
44 has_many :time_entries, :dependent => :delete_all
45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
46 has_many :documents, :dependent => :destroy
46 has_many :documents, :dependent => :destroy
47 has_many :news, :dependent => :destroy, :include => :author
47 has_many :news, :dependent => :destroy, :include => :author
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 has_one :repository, :conditions => ["is_default = ?", true]
50 has_one :repository, :conditions => ["is_default = ?", true]
51 has_many :repositories, :dependent => :destroy
51 has_many :repositories, :dependent => :destroy
52 has_many :changesets, :through => :repository
52 has_many :changesets, :through => :repository
53 has_one :wiki, :dependent => :destroy
53 has_one :wiki, :dependent => :destroy
54 # Custom field for the project issues
54 # Custom field for the project issues
55 has_and_belongs_to_many :issue_custom_fields,
55 has_and_belongs_to_many :issue_custom_fields,
56 :class_name => 'IssueCustomField',
56 :class_name => 'IssueCustomField',
57 :order => "#{CustomField.table_name}.position",
57 :order => "#{CustomField.table_name}.position",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
59 :association_foreign_key => 'custom_field_id'
59 :association_foreign_key => 'custom_field_id'
60
60
61 acts_as_nested_set :order => 'name', :dependent => :destroy
61 acts_as_nested_set :order => 'name', :dependent => :destroy
62 acts_as_attachable :view_permission => :view_files,
62 acts_as_attachable :view_permission => :view_files,
63 :delete_permission => :manage_files
63 :delete_permission => :manage_files
64
64
65 acts_as_customizable
65 acts_as_customizable
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 :author => nil
69 :author => nil
70
70
71 attr_protected :status
71 attr_protected :status
72
72
73 validates_presence_of :name, :identifier
73 validates_presence_of :name, :identifier
74 validates_uniqueness_of :identifier
74 validates_uniqueness_of :identifier
75 validates_associated :repository, :wiki
75 validates_associated :repository, :wiki
76 validates_length_of :name, :maximum => 255
76 validates_length_of :name, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
77 validates_length_of :homepage, :maximum => 255
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 # donwcase letters, digits, dashes but not digits only
79 # donwcase letters, digits, dashes but not digits only
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 # reserved words
81 # reserved words
82 validates_exclusion_of :identifier, :in => %w( new )
82 validates_exclusion_of :identifier, :in => %w( new )
83
83
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
86 before_destroy :delete_all_members
86 before_destroy :delete_all_members
87
87
88 scope :has_module, lambda {|mod|
88 scope :has_module, lambda {|mod|
89 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
90 }
90 }
91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
92 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
92 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
93 scope :all_public, lambda { where(:is_public => true) }
93 scope :all_public, lambda { where(:is_public => true) }
94 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
94 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
95 scope :allowed_to, lambda {|*args|
95 scope :allowed_to, lambda {|*args|
96 user = User.current
96 user = User.current
97 permission = nil
97 permission = nil
98 if args.first.is_a?(Symbol)
98 if args.first.is_a?(Symbol)
99 permission = args.shift
99 permission = args.shift
100 else
100 else
101 user = args.shift
101 user = args.shift
102 permission = args.shift
102 permission = args.shift
103 end
103 end
104 where(Project.allowed_to_condition(user, permission, *args))
104 where(Project.allowed_to_condition(user, permission, *args))
105 }
105 }
106 scope :like, lambda {|arg|
106 scope :like, lambda {|arg|
107 if arg.blank?
107 if arg.blank?
108 where(nil)
108 where(nil)
109 else
109 else
110 pattern = "%#{arg.to_s.strip.downcase}%"
110 pattern = "%#{arg.to_s.strip.downcase}%"
111 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
111 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
112 end
112 end
113 }
113 }
114
114
115 def initialize(attributes=nil, *args)
115 def initialize(attributes=nil, *args)
116 super
116 super
117
117
118 initialized = (attributes || {}).stringify_keys
118 initialized = (attributes || {}).stringify_keys
119 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
119 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
120 self.identifier = Project.next_identifier
120 self.identifier = Project.next_identifier
121 end
121 end
122 if !initialized.key?('is_public')
122 if !initialized.key?('is_public')
123 self.is_public = Setting.default_projects_public?
123 self.is_public = Setting.default_projects_public?
124 end
124 end
125 if !initialized.key?('enabled_module_names')
125 if !initialized.key?('enabled_module_names')
126 self.enabled_module_names = Setting.default_projects_modules
126 self.enabled_module_names = Setting.default_projects_modules
127 end
127 end
128 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
128 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
129 default = Setting.default_projects_tracker_ids
129 default = Setting.default_projects_tracker_ids
130 if default.is_a?(Array)
130 if default.is_a?(Array)
131 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
131 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
132 else
132 else
133 self.trackers = Tracker.sorted.all
133 self.trackers = Tracker.sorted.all
134 end
134 end
135 end
135 end
136 end
136 end
137
137
138 def identifier=(identifier)
138 def identifier=(identifier)
139 super unless identifier_frozen?
139 super unless identifier_frozen?
140 end
140 end
141
141
142 def identifier_frozen?
142 def identifier_frozen?
143 errors[:identifier].blank? && !(new_record? || identifier.blank?)
143 errors[:identifier].blank? && !(new_record? || identifier.blank?)
144 end
144 end
145
145
146 # returns latest created projects
146 # returns latest created projects
147 # non public projects will be returned only if user is a member of those
147 # non public projects will be returned only if user is a member of those
148 def self.latest(user=nil, count=5)
148 def self.latest(user=nil, count=5)
149 visible(user).limit(count).order("created_on DESC").all
149 visible(user).limit(count).order("created_on DESC").all
150 end
150 end
151
151
152 # Returns true if the project is visible to +user+ or to the current user.
152 # Returns true if the project is visible to +user+ or to the current user.
153 def visible?(user=User.current)
153 def visible?(user=User.current)
154 user.allowed_to?(:view_project, self)
154 user.allowed_to?(:view_project, self)
155 end
155 end
156
156
157 # Returns a SQL conditions string used to find all projects visible by the specified user.
157 # Returns a SQL conditions string used to find all projects visible by the specified user.
158 #
158 #
159 # Examples:
159 # Examples:
160 # Project.visible_condition(admin) => "projects.status = 1"
160 # Project.visible_condition(admin) => "projects.status = 1"
161 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
161 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
162 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
162 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
163 def self.visible_condition(user, options={})
163 def self.visible_condition(user, options={})
164 allowed_to_condition(user, :view_project, options)
164 allowed_to_condition(user, :view_project, options)
165 end
165 end
166
166
167 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
167 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
168 #
168 #
169 # Valid options:
169 # Valid options:
170 # * :project => limit the condition to project
170 # * :project => limit the condition to project
171 # * :with_subprojects => limit the condition to project and its subprojects
171 # * :with_subprojects => limit the condition to project and its subprojects
172 # * :member => limit the condition to the user projects
172 # * :member => limit the condition to the user projects
173 def self.allowed_to_condition(user, permission, options={})
173 def self.allowed_to_condition(user, permission, options={})
174 perm = Redmine::AccessControl.permission(permission)
174 perm = Redmine::AccessControl.permission(permission)
175 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
175 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
176 if perm && perm.project_module
176 if perm && perm.project_module
177 # If the permission belongs to a project module, make sure the module is enabled
177 # If the permission belongs to a project module, make sure the module is enabled
178 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
178 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
179 end
179 end
180 if options[:project]
180 if options[:project]
181 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
181 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
182 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
182 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
183 base_statement = "(#{project_statement}) AND (#{base_statement})"
183 base_statement = "(#{project_statement}) AND (#{base_statement})"
184 end
184 end
185
185
186 if user.admin?
186 if user.admin?
187 base_statement
187 base_statement
188 else
188 else
189 statement_by_role = {}
189 statement_by_role = {}
190 unless options[:member]
190 unless options[:member]
191 role = user.logged? ? Role.non_member : Role.anonymous
191 role = user.logged? ? Role.non_member : Role.anonymous
192 if role.allowed_to?(permission)
192 if role.allowed_to?(permission)
193 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
193 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
194 end
194 end
195 end
195 end
196 if user.logged?
196 if user.logged?
197 user.projects_by_role.each do |role, projects|
197 user.projects_by_role.each do |role, projects|
198 if role.allowed_to?(permission) && projects.any?
198 if role.allowed_to?(permission) && projects.any?
199 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
199 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
200 end
200 end
201 end
201 end
202 end
202 end
203 if statement_by_role.empty?
203 if statement_by_role.empty?
204 "1=0"
204 "1=0"
205 else
205 else
206 if block_given?
206 if block_given?
207 statement_by_role.each do |role, statement|
207 statement_by_role.each do |role, statement|
208 if s = yield(role, user)
208 if s = yield(role, user)
209 statement_by_role[role] = "(#{statement} AND (#{s}))"
209 statement_by_role[role] = "(#{statement} AND (#{s}))"
210 end
210 end
211 end
211 end
212 end
212 end
213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
214 end
214 end
215 end
215 end
216 end
216 end
217
217
218 # Returns the Systemwide and project specific activities
218 # Returns the Systemwide and project specific activities
219 def activities(include_inactive=false)
219 def activities(include_inactive=false)
220 if include_inactive
220 if include_inactive
221 return all_activities
221 return all_activities
222 else
222 else
223 return active_activities
223 return active_activities
224 end
224 end
225 end
225 end
226
226
227 # Will create a new Project specific Activity or update an existing one
227 # Will create a new Project specific Activity or update an existing one
228 #
228 #
229 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
229 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
230 # does not successfully save.
230 # does not successfully save.
231 def update_or_create_time_entry_activity(id, activity_hash)
231 def update_or_create_time_entry_activity(id, activity_hash)
232 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
232 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
233 self.create_time_entry_activity_if_needed(activity_hash)
233 self.create_time_entry_activity_if_needed(activity_hash)
234 else
234 else
235 activity = project.time_entry_activities.find_by_id(id.to_i)
235 activity = project.time_entry_activities.find_by_id(id.to_i)
236 activity.update_attributes(activity_hash) if activity
236 activity.update_attributes(activity_hash) if activity
237 end
237 end
238 end
238 end
239
239
240 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
240 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
241 #
241 #
242 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
242 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
243 # does not successfully save.
243 # does not successfully save.
244 def create_time_entry_activity_if_needed(activity)
244 def create_time_entry_activity_if_needed(activity)
245 if activity['parent_id']
245 if activity['parent_id']
246
246
247 parent_activity = TimeEntryActivity.find(activity['parent_id'])
247 parent_activity = TimeEntryActivity.find(activity['parent_id'])
248 activity['name'] = parent_activity.name
248 activity['name'] = parent_activity.name
249 activity['position'] = parent_activity.position
249 activity['position'] = parent_activity.position
250
250
251 if Enumeration.overridding_change?(activity, parent_activity)
251 if Enumeration.overridding_change?(activity, parent_activity)
252 project_activity = self.time_entry_activities.create(activity)
252 project_activity = self.time_entry_activities.create(activity)
253
253
254 if project_activity.new_record?
254 if project_activity.new_record?
255 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
255 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
256 else
256 else
257 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
257 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
258 end
258 end
259 end
259 end
260 end
260 end
261 end
261 end
262
262
263 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
263 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
264 #
264 #
265 # Examples:
265 # Examples:
266 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
266 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
267 # project.project_condition(false) => "projects.id = 1"
267 # project.project_condition(false) => "projects.id = 1"
268 def project_condition(with_subprojects)
268 def project_condition(with_subprojects)
269 cond = "#{Project.table_name}.id = #{id}"
269 cond = "#{Project.table_name}.id = #{id}"
270 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
270 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
271 cond
271 cond
272 end
272 end
273
273
274 def self.find(*args)
274 def self.find(*args)
275 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
275 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
276 project = find_by_identifier(*args)
276 project = find_by_identifier(*args)
277 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
277 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
278 project
278 project
279 else
279 else
280 super
280 super
281 end
281 end
282 end
282 end
283
283
284 def self.find_by_param(*args)
284 def self.find_by_param(*args)
285 self.find(*args)
285 self.find(*args)
286 end
286 end
287
287
288 alias :base_reload :reload
288 def reload(*args)
289 def reload(*args)
289 @shared_versions = nil
290 @shared_versions = nil
290 @rolled_up_versions = nil
291 @rolled_up_versions = nil
291 @rolled_up_trackers = nil
292 @rolled_up_trackers = nil
292 @all_issue_custom_fields = nil
293 @all_issue_custom_fields = nil
293 @all_time_entry_custom_fields = nil
294 @all_time_entry_custom_fields = nil
294 @to_param = nil
295 @to_param = nil
295 @allowed_parents = nil
296 @allowed_parents = nil
296 @allowed_permissions = nil
297 @allowed_permissions = nil
297 @actions_allowed = nil
298 @actions_allowed = nil
298 @start_date = nil
299 @start_date = nil
299 @due_date = nil
300 @due_date = nil
300 super
301 base_reload(*args)
301 end
302 end
302
303
303 def to_param
304 def to_param
304 # id is used for projects with a numeric identifier (compatibility)
305 # id is used for projects with a numeric identifier (compatibility)
305 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
306 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
306 end
307 end
307
308
308 def active?
309 def active?
309 self.status == STATUS_ACTIVE
310 self.status == STATUS_ACTIVE
310 end
311 end
311
312
312 def archived?
313 def archived?
313 self.status == STATUS_ARCHIVED
314 self.status == STATUS_ARCHIVED
314 end
315 end
315
316
316 # Archives the project and its descendants
317 # Archives the project and its descendants
317 def archive
318 def archive
318 # Check that there is no issue of a non descendant project that is assigned
319 # Check that there is no issue of a non descendant project that is assigned
319 # to one of the project or descendant versions
320 # to one of the project or descendant versions
320 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
321 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
321 if v_ids.any? &&
322 if v_ids.any? &&
322 Issue.
323 Issue.
323 includes(:project).
324 includes(:project).
324 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
325 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
325 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
326 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
326 exists?
327 exists?
327 return false
328 return false
328 end
329 end
329 Project.transaction do
330 Project.transaction do
330 archive!
331 archive!
331 end
332 end
332 true
333 true
333 end
334 end
334
335
335 # Unarchives the project
336 # Unarchives the project
336 # All its ancestors must be active
337 # All its ancestors must be active
337 def unarchive
338 def unarchive
338 return false if ancestors.detect {|a| !a.active?}
339 return false if ancestors.detect {|a| !a.active?}
339 update_attribute :status, STATUS_ACTIVE
340 update_attribute :status, STATUS_ACTIVE
340 end
341 end
341
342
342 def close
343 def close
343 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
344 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
344 end
345 end
345
346
346 def reopen
347 def reopen
347 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
348 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
348 end
349 end
349
350
350 # Returns an array of projects the project can be moved to
351 # Returns an array of projects the project can be moved to
351 # by the current user
352 # by the current user
352 def allowed_parents
353 def allowed_parents
353 return @allowed_parents if @allowed_parents
354 return @allowed_parents if @allowed_parents
354 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
355 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
355 @allowed_parents = @allowed_parents - self_and_descendants
356 @allowed_parents = @allowed_parents - self_and_descendants
356 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
357 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
357 @allowed_parents << nil
358 @allowed_parents << nil
358 end
359 end
359 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
360 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
360 @allowed_parents << parent
361 @allowed_parents << parent
361 end
362 end
362 @allowed_parents
363 @allowed_parents
363 end
364 end
364
365
365 # Sets the parent of the project with authorization check
366 # Sets the parent of the project with authorization check
366 def set_allowed_parent!(p)
367 def set_allowed_parent!(p)
367 unless p.nil? || p.is_a?(Project)
368 unless p.nil? || p.is_a?(Project)
368 if p.to_s.blank?
369 if p.to_s.blank?
369 p = nil
370 p = nil
370 else
371 else
371 p = Project.find_by_id(p)
372 p = Project.find_by_id(p)
372 return false unless p
373 return false unless p
373 end
374 end
374 end
375 end
375 if p.nil?
376 if p.nil?
376 if !new_record? && allowed_parents.empty?
377 if !new_record? && allowed_parents.empty?
377 return false
378 return false
378 end
379 end
379 elsif !allowed_parents.include?(p)
380 elsif !allowed_parents.include?(p)
380 return false
381 return false
381 end
382 end
382 set_parent!(p)
383 set_parent!(p)
383 end
384 end
384
385
385 # Sets the parent of the project
386 # Sets the parent of the project
386 # Argument can be either a Project, a String, a Fixnum or nil
387 # Argument can be either a Project, a String, a Fixnum or nil
387 def set_parent!(p)
388 def set_parent!(p)
388 unless p.nil? || p.is_a?(Project)
389 unless p.nil? || p.is_a?(Project)
389 if p.to_s.blank?
390 if p.to_s.blank?
390 p = nil
391 p = nil
391 else
392 else
392 p = Project.find_by_id(p)
393 p = Project.find_by_id(p)
393 return false unless p
394 return false unless p
394 end
395 end
395 end
396 end
396 if p == parent && !p.nil?
397 if p == parent && !p.nil?
397 # Nothing to do
398 # Nothing to do
398 true
399 true
399 elsif p.nil? || (p.active? && move_possible?(p))
400 elsif p.nil? || (p.active? && move_possible?(p))
400 set_or_update_position_under(p)
401 set_or_update_position_under(p)
401 Issue.update_versions_from_hierarchy_change(self)
402 Issue.update_versions_from_hierarchy_change(self)
402 true
403 true
403 else
404 else
404 # Can not move to the given target
405 # Can not move to the given target
405 false
406 false
406 end
407 end
407 end
408 end
408
409
409 # Recalculates all lft and rgt values based on project names
410 # Recalculates all lft and rgt values based on project names
410 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
411 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
411 # Used in BuildProjectsTree migration
412 # Used in BuildProjectsTree migration
412 def self.rebuild_tree!
413 def self.rebuild_tree!
413 transaction do
414 transaction do
414 update_all "lft = NULL, rgt = NULL"
415 update_all "lft = NULL, rgt = NULL"
415 rebuild!(false)
416 rebuild!(false)
416 end
417 end
417 end
418 end
418
419
419 # Returns an array of the trackers used by the project and its active sub projects
420 # Returns an array of the trackers used by the project and its active sub projects
420 def rolled_up_trackers
421 def rolled_up_trackers
421 @rolled_up_trackers ||=
422 @rolled_up_trackers ||=
422 Tracker.
423 Tracker.
423 joins(:projects).
424 joins(:projects).
424 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
425 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
425 select("DISTINCT #{Tracker.table_name}.*").
426 select("DISTINCT #{Tracker.table_name}.*").
426 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
427 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
427 sorted.
428 sorted.
428 all
429 all
429 end
430 end
430
431
431 # Closes open and locked project versions that are completed
432 # Closes open and locked project versions that are completed
432 def close_completed_versions
433 def close_completed_versions
433 Version.transaction do
434 Version.transaction do
434 versions.where(:status => %w(open locked)).all.each do |version|
435 versions.where(:status => %w(open locked)).all.each do |version|
435 if version.completed?
436 if version.completed?
436 version.update_attribute(:status, 'closed')
437 version.update_attribute(:status, 'closed')
437 end
438 end
438 end
439 end
439 end
440 end
440 end
441 end
441
442
442 # Returns a scope of the Versions on subprojects
443 # Returns a scope of the Versions on subprojects
443 def rolled_up_versions
444 def rolled_up_versions
444 @rolled_up_versions ||=
445 @rolled_up_versions ||=
445 Version.scoped(:include => :project,
446 Version.scoped(:include => :project,
446 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
447 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
447 end
448 end
448
449
449 # Returns a scope of the Versions used by the project
450 # Returns a scope of the Versions used by the project
450 def shared_versions
451 def shared_versions
451 if new_record?
452 if new_record?
452 Version.scoped(:include => :project,
453 Version.scoped(:include => :project,
453 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
454 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
454 else
455 else
455 @shared_versions ||= begin
456 @shared_versions ||= begin
456 r = root? ? self : root
457 r = root? ? self : root
457 Version.scoped(:include => :project,
458 Version.scoped(:include => :project,
458 :conditions => "#{Project.table_name}.id = #{id}" +
459 :conditions => "#{Project.table_name}.id = #{id}" +
459 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
460 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
460 " #{Version.table_name}.sharing = 'system'" +
461 " #{Version.table_name}.sharing = 'system'" +
461 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
462 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
462 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
463 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
463 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
464 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
464 "))")
465 "))")
465 end
466 end
466 end
467 end
467 end
468 end
468
469
469 # Returns a hash of project users grouped by role
470 # Returns a hash of project users grouped by role
470 def users_by_role
471 def users_by_role
471 members.includes(:user, :roles).all.inject({}) do |h, m|
472 members.includes(:user, :roles).all.inject({}) do |h, m|
472 m.roles.each do |r|
473 m.roles.each do |r|
473 h[r] ||= []
474 h[r] ||= []
474 h[r] << m.user
475 h[r] << m.user
475 end
476 end
476 h
477 h
477 end
478 end
478 end
479 end
479
480
480 # Deletes all project's members
481 # Deletes all project's members
481 def delete_all_members
482 def delete_all_members
482 me, mr = Member.table_name, MemberRole.table_name
483 me, mr = Member.table_name, MemberRole.table_name
483 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
484 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
484 Member.delete_all(['project_id = ?', id])
485 Member.delete_all(['project_id = ?', id])
485 end
486 end
486
487
487 # Users/groups issues can be assigned to
488 # Users/groups issues can be assigned to
488 def assignable_users
489 def assignable_users
489 assignable = Setting.issue_group_assignment? ? member_principals : members
490 assignable = Setting.issue_group_assignment? ? member_principals : members
490 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
491 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
491 end
492 end
492
493
493 # Returns the mail adresses of users that should be always notified on project events
494 # Returns the mail adresses of users that should be always notified on project events
494 def recipients
495 def recipients
495 notified_users.collect {|user| user.mail}
496 notified_users.collect {|user| user.mail}
496 end
497 end
497
498
498 # Returns the users that should be notified on project events
499 # Returns the users that should be notified on project events
499 def notified_users
500 def notified_users
500 # TODO: User part should be extracted to User#notify_about?
501 # TODO: User part should be extracted to User#notify_about?
501 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
502 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
502 end
503 end
503
504
504 # Returns an array of all custom fields enabled for project issues
505 # Returns an array of all custom fields enabled for project issues
505 # (explictly associated custom fields and custom fields enabled for all projects)
506 # (explictly associated custom fields and custom fields enabled for all projects)
506 def all_issue_custom_fields
507 def all_issue_custom_fields
507 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
508 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
508 end
509 end
509
510
510 # Returns an array of all custom fields enabled for project time entries
511 # Returns an array of all custom fields enabled for project time entries
511 # (explictly associated custom fields and custom fields enabled for all projects)
512 # (explictly associated custom fields and custom fields enabled for all projects)
512 def all_time_entry_custom_fields
513 def all_time_entry_custom_fields
513 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
514 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
514 end
515 end
515
516
516 def project
517 def project
517 self
518 self
518 end
519 end
519
520
520 def <=>(project)
521 def <=>(project)
521 name.downcase <=> project.name.downcase
522 name.downcase <=> project.name.downcase
522 end
523 end
523
524
524 def to_s
525 def to_s
525 name
526 name
526 end
527 end
527
528
528 # Returns a short description of the projects (first lines)
529 # Returns a short description of the projects (first lines)
529 def short_description(length = 255)
530 def short_description(length = 255)
530 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
531 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
531 end
532 end
532
533
533 def css_classes
534 def css_classes
534 s = 'project'
535 s = 'project'
535 s << ' root' if root?
536 s << ' root' if root?
536 s << ' child' if child?
537 s << ' child' if child?
537 s << (leaf? ? ' leaf' : ' parent')
538 s << (leaf? ? ' leaf' : ' parent')
538 unless active?
539 unless active?
539 if archived?
540 if archived?
540 s << ' archived'
541 s << ' archived'
541 else
542 else
542 s << ' closed'
543 s << ' closed'
543 end
544 end
544 end
545 end
545 s
546 s
546 end
547 end
547
548
548 # The earliest start date of a project, based on it's issues and versions
549 # The earliest start date of a project, based on it's issues and versions
549 def start_date
550 def start_date
550 @start_date ||= [
551 @start_date ||= [
551 issues.minimum('start_date'),
552 issues.minimum('start_date'),
552 shared_versions.minimum('effective_date'),
553 shared_versions.minimum('effective_date'),
553 Issue.fixed_version(shared_versions).minimum('start_date')
554 Issue.fixed_version(shared_versions).minimum('start_date')
554 ].compact.min
555 ].compact.min
555 end
556 end
556
557
557 # The latest due date of an issue or version
558 # The latest due date of an issue or version
558 def due_date
559 def due_date
559 @due_date ||= [
560 @due_date ||= [
560 issues.maximum('due_date'),
561 issues.maximum('due_date'),
561 shared_versions.maximum('effective_date'),
562 shared_versions.maximum('effective_date'),
562 Issue.fixed_version(shared_versions).maximum('due_date')
563 Issue.fixed_version(shared_versions).maximum('due_date')
563 ].compact.max
564 ].compact.max
564 end
565 end
565
566
566 def overdue?
567 def overdue?
567 active? && !due_date.nil? && (due_date < Date.today)
568 active? && !due_date.nil? && (due_date < Date.today)
568 end
569 end
569
570
570 # Returns the percent completed for this project, based on the
571 # Returns the percent completed for this project, based on the
571 # progress on it's versions.
572 # progress on it's versions.
572 def completed_percent(options={:include_subprojects => false})
573 def completed_percent(options={:include_subprojects => false})
573 if options.delete(:include_subprojects)
574 if options.delete(:include_subprojects)
574 total = self_and_descendants.collect(&:completed_percent).sum
575 total = self_and_descendants.collect(&:completed_percent).sum
575
576
576 total / self_and_descendants.count
577 total / self_and_descendants.count
577 else
578 else
578 if versions.count > 0
579 if versions.count > 0
579 total = versions.collect(&:completed_percent).sum
580 total = versions.collect(&:completed_percent).sum
580
581
581 total / versions.count
582 total / versions.count
582 else
583 else
583 100
584 100
584 end
585 end
585 end
586 end
586 end
587 end
587
588
588 # Return true if this project allows to do the specified action.
589 # Return true if this project allows to do the specified action.
589 # action can be:
590 # action can be:
590 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
591 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
591 # * a permission Symbol (eg. :edit_project)
592 # * a permission Symbol (eg. :edit_project)
592 def allows_to?(action)
593 def allows_to?(action)
593 if archived?
594 if archived?
594 # No action allowed on archived projects
595 # No action allowed on archived projects
595 return false
596 return false
596 end
597 end
597 unless active? || Redmine::AccessControl.read_action?(action)
598 unless active? || Redmine::AccessControl.read_action?(action)
598 # No write action allowed on closed projects
599 # No write action allowed on closed projects
599 return false
600 return false
600 end
601 end
601 # No action allowed on disabled modules
602 # No action allowed on disabled modules
602 if action.is_a? Hash
603 if action.is_a? Hash
603 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
604 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
604 else
605 else
605 allowed_permissions.include? action
606 allowed_permissions.include? action
606 end
607 end
607 end
608 end
608
609
609 def module_enabled?(module_name)
610 def module_enabled?(module_name)
610 module_name = module_name.to_s
611 module_name = module_name.to_s
611 enabled_modules.detect {|m| m.name == module_name}
612 enabled_modules.detect {|m| m.name == module_name}
612 end
613 end
613
614
614 def enabled_module_names=(module_names)
615 def enabled_module_names=(module_names)
615 if module_names && module_names.is_a?(Array)
616 if module_names && module_names.is_a?(Array)
616 module_names = module_names.collect(&:to_s).reject(&:blank?)
617 module_names = module_names.collect(&:to_s).reject(&:blank?)
617 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
618 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
618 else
619 else
619 enabled_modules.clear
620 enabled_modules.clear
620 end
621 end
621 end
622 end
622
623
623 # Returns an array of the enabled modules names
624 # Returns an array of the enabled modules names
624 def enabled_module_names
625 def enabled_module_names
625 enabled_modules.collect(&:name)
626 enabled_modules.collect(&:name)
626 end
627 end
627
628
628 # Enable a specific module
629 # Enable a specific module
629 #
630 #
630 # Examples:
631 # Examples:
631 # project.enable_module!(:issue_tracking)
632 # project.enable_module!(:issue_tracking)
632 # project.enable_module!("issue_tracking")
633 # project.enable_module!("issue_tracking")
633 def enable_module!(name)
634 def enable_module!(name)
634 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
635 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
635 end
636 end
636
637
637 # Disable a module if it exists
638 # Disable a module if it exists
638 #
639 #
639 # Examples:
640 # Examples:
640 # project.disable_module!(:issue_tracking)
641 # project.disable_module!(:issue_tracking)
641 # project.disable_module!("issue_tracking")
642 # project.disable_module!("issue_tracking")
642 # project.disable_module!(project.enabled_modules.first)
643 # project.disable_module!(project.enabled_modules.first)
643 def disable_module!(target)
644 def disable_module!(target)
644 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
645 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
645 target.destroy unless target.blank?
646 target.destroy unless target.blank?
646 end
647 end
647
648
648 safe_attributes 'name',
649 safe_attributes 'name',
649 'description',
650 'description',
650 'homepage',
651 'homepage',
651 'is_public',
652 'is_public',
652 'identifier',
653 'identifier',
653 'custom_field_values',
654 'custom_field_values',
654 'custom_fields',
655 'custom_fields',
655 'tracker_ids',
656 'tracker_ids',
656 'issue_custom_field_ids'
657 'issue_custom_field_ids'
657
658
658 safe_attributes 'enabled_module_names',
659 safe_attributes 'enabled_module_names',
659 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
660 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
660
661
661 safe_attributes 'inherit_members',
662 safe_attributes 'inherit_members',
662 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
663 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
663
664
664 # Returns an array of projects that are in this project's hierarchy
665 # Returns an array of projects that are in this project's hierarchy
665 #
666 #
666 # Example: parents, children, siblings
667 # Example: parents, children, siblings
667 def hierarchy
668 def hierarchy
668 parents = project.self_and_ancestors || []
669 parents = project.self_and_ancestors || []
669 descendants = project.descendants || []
670 descendants = project.descendants || []
670 project_hierarchy = parents | descendants # Set union
671 project_hierarchy = parents | descendants # Set union
671 end
672 end
672
673
673 # Returns an auto-generated project identifier based on the last identifier used
674 # Returns an auto-generated project identifier based on the last identifier used
674 def self.next_identifier
675 def self.next_identifier
675 p = Project.order('created_on DESC').first
676 p = Project.order('created_on DESC').first
676 p.nil? ? nil : p.identifier.to_s.succ
677 p.nil? ? nil : p.identifier.to_s.succ
677 end
678 end
678
679
679 # Copies and saves the Project instance based on the +project+.
680 # Copies and saves the Project instance based on the +project+.
680 # Duplicates the source project's:
681 # Duplicates the source project's:
681 # * Wiki
682 # * Wiki
682 # * Versions
683 # * Versions
683 # * Categories
684 # * Categories
684 # * Issues
685 # * Issues
685 # * Members
686 # * Members
686 # * Queries
687 # * Queries
687 #
688 #
688 # Accepts an +options+ argument to specify what to copy
689 # Accepts an +options+ argument to specify what to copy
689 #
690 #
690 # Examples:
691 # Examples:
691 # project.copy(1) # => copies everything
692 # project.copy(1) # => copies everything
692 # project.copy(1, :only => 'members') # => copies members only
693 # project.copy(1, :only => 'members') # => copies members only
693 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
694 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
694 def copy(project, options={})
695 def copy(project, options={})
695 project = project.is_a?(Project) ? project : Project.find(project)
696 project = project.is_a?(Project) ? project : Project.find(project)
696
697
697 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
698 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
698 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
699 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
699
700
700 Project.transaction do
701 Project.transaction do
701 if save
702 if save
702 reload
703 reload
703 to_be_copied.each do |name|
704 to_be_copied.each do |name|
704 send "copy_#{name}", project
705 send "copy_#{name}", project
705 end
706 end
706 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
707 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
707 save
708 save
708 end
709 end
709 end
710 end
710 end
711 end
711
712
712 # Returns a new unsaved Project instance with attributes copied from +project+
713 # Returns a new unsaved Project instance with attributes copied from +project+
713 def self.copy_from(project)
714 def self.copy_from(project)
714 project = project.is_a?(Project) ? project : Project.find(project)
715 project = project.is_a?(Project) ? project : Project.find(project)
715 # clear unique attributes
716 # clear unique attributes
716 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
717 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
717 copy = Project.new(attributes)
718 copy = Project.new(attributes)
718 copy.enabled_modules = project.enabled_modules
719 copy.enabled_modules = project.enabled_modules
719 copy.trackers = project.trackers
720 copy.trackers = project.trackers
720 copy.custom_values = project.custom_values.collect {|v| v.clone}
721 copy.custom_values = project.custom_values.collect {|v| v.clone}
721 copy.issue_custom_fields = project.issue_custom_fields
722 copy.issue_custom_fields = project.issue_custom_fields
722 copy
723 copy
723 end
724 end
724
725
725 # Yields the given block for each project with its level in the tree
726 # Yields the given block for each project with its level in the tree
726 def self.project_tree(projects, &block)
727 def self.project_tree(projects, &block)
727 ancestors = []
728 ancestors = []
728 projects.sort_by(&:lft).each do |project|
729 projects.sort_by(&:lft).each do |project|
729 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
730 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
730 ancestors.pop
731 ancestors.pop
731 end
732 end
732 yield project, ancestors.size
733 yield project, ancestors.size
733 ancestors << project
734 ancestors << project
734 end
735 end
735 end
736 end
736
737
737 private
738 private
738
739
739 def after_parent_changed(parent_was)
740 def after_parent_changed(parent_was)
740 remove_inherited_member_roles
741 remove_inherited_member_roles
741 add_inherited_member_roles
742 add_inherited_member_roles
742 end
743 end
743
744
744 def update_inherited_members
745 def update_inherited_members
745 if parent
746 if parent
746 if inherit_members? && !inherit_members_was
747 if inherit_members? && !inherit_members_was
747 remove_inherited_member_roles
748 remove_inherited_member_roles
748 add_inherited_member_roles
749 add_inherited_member_roles
749 elsif !inherit_members? && inherit_members_was
750 elsif !inherit_members? && inherit_members_was
750 remove_inherited_member_roles
751 remove_inherited_member_roles
751 end
752 end
752 end
753 end
753 end
754 end
754
755
755 def remove_inherited_member_roles
756 def remove_inherited_member_roles
756 member_roles = memberships.map(&:member_roles).flatten
757 member_roles = memberships.map(&:member_roles).flatten
757 member_role_ids = member_roles.map(&:id)
758 member_role_ids = member_roles.map(&:id)
758 member_roles.each do |member_role|
759 member_roles.each do |member_role|
759 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
760 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
760 member_role.destroy
761 member_role.destroy
761 end
762 end
762 end
763 end
763 end
764 end
764
765
765 def add_inherited_member_roles
766 def add_inherited_member_roles
766 if inherit_members? && parent
767 if inherit_members? && parent
767 parent.memberships.each do |parent_member|
768 parent.memberships.each do |parent_member|
768 member = Member.find_or_new(self.id, parent_member.user_id)
769 member = Member.find_or_new(self.id, parent_member.user_id)
769 parent_member.member_roles.each do |parent_member_role|
770 parent_member.member_roles.each do |parent_member_role|
770 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
771 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
771 end
772 end
772 member.save!
773 member.save!
773 end
774 end
774 end
775 end
775 end
776 end
776
777
777 # Copies wiki from +project+
778 # Copies wiki from +project+
778 def copy_wiki(project)
779 def copy_wiki(project)
779 # Check that the source project has a wiki first
780 # Check that the source project has a wiki first
780 unless project.wiki.nil?
781 unless project.wiki.nil?
781 wiki = self.wiki || Wiki.new
782 wiki = self.wiki || Wiki.new
782 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
783 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
783 wiki_pages_map = {}
784 wiki_pages_map = {}
784 project.wiki.pages.each do |page|
785 project.wiki.pages.each do |page|
785 # Skip pages without content
786 # Skip pages without content
786 next if page.content.nil?
787 next if page.content.nil?
787 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
788 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
788 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
789 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
789 new_wiki_page.content = new_wiki_content
790 new_wiki_page.content = new_wiki_content
790 wiki.pages << new_wiki_page
791 wiki.pages << new_wiki_page
791 wiki_pages_map[page.id] = new_wiki_page
792 wiki_pages_map[page.id] = new_wiki_page
792 end
793 end
793
794
794 self.wiki = wiki
795 self.wiki = wiki
795 wiki.save
796 wiki.save
796 # Reproduce page hierarchy
797 # Reproduce page hierarchy
797 project.wiki.pages.each do |page|
798 project.wiki.pages.each do |page|
798 if page.parent_id && wiki_pages_map[page.id]
799 if page.parent_id && wiki_pages_map[page.id]
799 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
800 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
800 wiki_pages_map[page.id].save
801 wiki_pages_map[page.id].save
801 end
802 end
802 end
803 end
803 end
804 end
804 end
805 end
805
806
806 # Copies versions from +project+
807 # Copies versions from +project+
807 def copy_versions(project)
808 def copy_versions(project)
808 project.versions.each do |version|
809 project.versions.each do |version|
809 new_version = Version.new
810 new_version = Version.new
810 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
811 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
811 self.versions << new_version
812 self.versions << new_version
812 end
813 end
813 end
814 end
814
815
815 # Copies issue categories from +project+
816 # Copies issue categories from +project+
816 def copy_issue_categories(project)
817 def copy_issue_categories(project)
817 project.issue_categories.each do |issue_category|
818 project.issue_categories.each do |issue_category|
818 new_issue_category = IssueCategory.new
819 new_issue_category = IssueCategory.new
819 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
820 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
820 self.issue_categories << new_issue_category
821 self.issue_categories << new_issue_category
821 end
822 end
822 end
823 end
823
824
824 # Copies issues from +project+
825 # Copies issues from +project+
825 def copy_issues(project)
826 def copy_issues(project)
826 # Stores the source issue id as a key and the copied issues as the
827 # Stores the source issue id as a key and the copied issues as the
827 # value. Used to map the two togeather for issue relations.
828 # value. Used to map the two togeather for issue relations.
828 issues_map = {}
829 issues_map = {}
829
830
830 # Store status and reopen locked/closed versions
831 # Store status and reopen locked/closed versions
831 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
832 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
832 version_statuses.each do |version, status|
833 version_statuses.each do |version, status|
833 version.update_attribute :status, 'open'
834 version.update_attribute :status, 'open'
834 end
835 end
835
836
836 # Get issues sorted by root_id, lft so that parent issues
837 # Get issues sorted by root_id, lft so that parent issues
837 # get copied before their children
838 # get copied before their children
838 project.issues.reorder('root_id, lft').all.each do |issue|
839 project.issues.reorder('root_id, lft').all.each do |issue|
839 new_issue = Issue.new
840 new_issue = Issue.new
840 new_issue.copy_from(issue, :subtasks => false, :link => false)
841 new_issue.copy_from(issue, :subtasks => false, :link => false)
841 new_issue.project = self
842 new_issue.project = self
842 # Reassign fixed_versions by name, since names are unique per project
843 # Reassign fixed_versions by name, since names are unique per project
843 if issue.fixed_version && issue.fixed_version.project == project
844 if issue.fixed_version && issue.fixed_version.project == project
844 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
845 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
845 end
846 end
846 # Reassign the category by name, since names are unique per project
847 # Reassign the category by name, since names are unique per project
847 if issue.category
848 if issue.category
848 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
849 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
849 end
850 end
850 # Parent issue
851 # Parent issue
851 if issue.parent_id
852 if issue.parent_id
852 if copied_parent = issues_map[issue.parent_id]
853 if copied_parent = issues_map[issue.parent_id]
853 new_issue.parent_issue_id = copied_parent.id
854 new_issue.parent_issue_id = copied_parent.id
854 end
855 end
855 end
856 end
856
857
857 self.issues << new_issue
858 self.issues << new_issue
858 if new_issue.new_record?
859 if new_issue.new_record?
859 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
860 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
860 else
861 else
861 issues_map[issue.id] = new_issue unless new_issue.new_record?
862 issues_map[issue.id] = new_issue unless new_issue.new_record?
862 end
863 end
863 end
864 end
864
865
865 # Restore locked/closed version statuses
866 # Restore locked/closed version statuses
866 version_statuses.each do |version, status|
867 version_statuses.each do |version, status|
867 version.update_attribute :status, status
868 version.update_attribute :status, status
868 end
869 end
869
870
870 # Relations after in case issues related each other
871 # Relations after in case issues related each other
871 project.issues.each do |issue|
872 project.issues.each do |issue|
872 new_issue = issues_map[issue.id]
873 new_issue = issues_map[issue.id]
873 unless new_issue
874 unless new_issue
874 # Issue was not copied
875 # Issue was not copied
875 next
876 next
876 end
877 end
877
878
878 # Relations
879 # Relations
879 issue.relations_from.each do |source_relation|
880 issue.relations_from.each do |source_relation|
880 new_issue_relation = IssueRelation.new
881 new_issue_relation = IssueRelation.new
881 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
882 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
882 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
883 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
883 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
884 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
884 new_issue_relation.issue_to = source_relation.issue_to
885 new_issue_relation.issue_to = source_relation.issue_to
885 end
886 end
886 new_issue.relations_from << new_issue_relation
887 new_issue.relations_from << new_issue_relation
887 end
888 end
888
889
889 issue.relations_to.each do |source_relation|
890 issue.relations_to.each do |source_relation|
890 new_issue_relation = IssueRelation.new
891 new_issue_relation = IssueRelation.new
891 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
892 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
892 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
893 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
893 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
894 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
894 new_issue_relation.issue_from = source_relation.issue_from
895 new_issue_relation.issue_from = source_relation.issue_from
895 end
896 end
896 new_issue.relations_to << new_issue_relation
897 new_issue.relations_to << new_issue_relation
897 end
898 end
898 end
899 end
899 end
900 end
900
901
901 # Copies members from +project+
902 # Copies members from +project+
902 def copy_members(project)
903 def copy_members(project)
903 # Copy users first, then groups to handle members with inherited and given roles
904 # Copy users first, then groups to handle members with inherited and given roles
904 members_to_copy = []
905 members_to_copy = []
905 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
906 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
906 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
907 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
907
908
908 members_to_copy.each do |member|
909 members_to_copy.each do |member|
909 new_member = Member.new
910 new_member = Member.new
910 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
911 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
911 # only copy non inherited roles
912 # only copy non inherited roles
912 # inherited roles will be added when copying the group membership
913 # inherited roles will be added when copying the group membership
913 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
914 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
914 next if role_ids.empty?
915 next if role_ids.empty?
915 new_member.role_ids = role_ids
916 new_member.role_ids = role_ids
916 new_member.project = self
917 new_member.project = self
917 self.members << new_member
918 self.members << new_member
918 end
919 end
919 end
920 end
920
921
921 # Copies queries from +project+
922 # Copies queries from +project+
922 def copy_queries(project)
923 def copy_queries(project)
923 project.queries.each do |query|
924 project.queries.each do |query|
924 new_query = IssueQuery.new
925 new_query = IssueQuery.new
925 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
926 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
926 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
927 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
927 new_query.project = self
928 new_query.project = self
928 new_query.user_id = query.user_id
929 new_query.user_id = query.user_id
929 self.queries << new_query
930 self.queries << new_query
930 end
931 end
931 end
932 end
932
933
933 # Copies boards from +project+
934 # Copies boards from +project+
934 def copy_boards(project)
935 def copy_boards(project)
935 project.boards.each do |board|
936 project.boards.each do |board|
936 new_board = Board.new
937 new_board = Board.new
937 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
938 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
938 new_board.project = self
939 new_board.project = self
939 self.boards << new_board
940 self.boards << new_board
940 end
941 end
941 end
942 end
942
943
943 def allowed_permissions
944 def allowed_permissions
944 @allowed_permissions ||= begin
945 @allowed_permissions ||= begin
945 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
946 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
946 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
947 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
947 end
948 end
948 end
949 end
949
950
950 def allowed_actions
951 def allowed_actions
951 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
952 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
952 end
953 end
953
954
954 # Returns all the active Systemwide and project specific activities
955 # Returns all the active Systemwide and project specific activities
955 def active_activities
956 def active_activities
956 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
957 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
957
958
958 if overridden_activity_ids.empty?
959 if overridden_activity_ids.empty?
959 return TimeEntryActivity.shared.active
960 return TimeEntryActivity.shared.active
960 else
961 else
961 return system_activities_and_project_overrides
962 return system_activities_and_project_overrides
962 end
963 end
963 end
964 end
964
965
965 # Returns all the Systemwide and project specific activities
966 # Returns all the Systemwide and project specific activities
966 # (inactive and active)
967 # (inactive and active)
967 def all_activities
968 def all_activities
968 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
969 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
969
970
970 if overridden_activity_ids.empty?
971 if overridden_activity_ids.empty?
971 return TimeEntryActivity.shared
972 return TimeEntryActivity.shared
972 else
973 else
973 return system_activities_and_project_overrides(true)
974 return system_activities_and_project_overrides(true)
974 end
975 end
975 end
976 end
976
977
977 # Returns the systemwide active activities merged with the project specific overrides
978 # Returns the systemwide active activities merged with the project specific overrides
978 def system_activities_and_project_overrides(include_inactive=false)
979 def system_activities_and_project_overrides(include_inactive=false)
979 if include_inactive
980 if include_inactive
980 return TimeEntryActivity.shared.
981 return TimeEntryActivity.shared.
981 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
982 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
982 self.time_entry_activities
983 self.time_entry_activities
983 else
984 else
984 return TimeEntryActivity.shared.active.
985 return TimeEntryActivity.shared.active.
985 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
986 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
986 self.time_entry_activities.active
987 self.time_entry_activities.active
987 end
988 end
988 end
989 end
989
990
990 # Archives subprojects recursively
991 # Archives subprojects recursively
991 def archive!
992 def archive!
992 children.each do |subproject|
993 children.each do |subproject|
993 subproject.send :archive!
994 subproject.send :archive!
994 end
995 end
995 update_attribute :status, STATUS_ARCHIVED
996 update_attribute :status, STATUS_ARCHIVED
996 end
997 end
997
998
998 def update_position_under_parent
999 def update_position_under_parent
999 set_or_update_position_under(parent)
1000 set_or_update_position_under(parent)
1000 end
1001 end
1001
1002
1002 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1003 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
1003 def set_or_update_position_under(target_parent)
1004 def set_or_update_position_under(target_parent)
1004 parent_was = parent
1005 parent_was = parent
1005 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1006 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1006 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1007 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1007
1008
1008 if to_be_inserted_before
1009 if to_be_inserted_before
1009 move_to_left_of(to_be_inserted_before)
1010 move_to_left_of(to_be_inserted_before)
1010 elsif target_parent.nil?
1011 elsif target_parent.nil?
1011 if sibs.empty?
1012 if sibs.empty?
1012 # move_to_root adds the project in first (ie. left) position
1013 # move_to_root adds the project in first (ie. left) position
1013 move_to_root
1014 move_to_root
1014 else
1015 else
1015 move_to_right_of(sibs.last) unless self == sibs.last
1016 move_to_right_of(sibs.last) unless self == sibs.last
1016 end
1017 end
1017 else
1018 else
1018 # move_to_child_of adds the project in last (ie.right) position
1019 # move_to_child_of adds the project in last (ie.right) position
1019 move_to_child_of(target_parent)
1020 move_to_child_of(target_parent)
1020 end
1021 end
1021 if parent_was != target_parent
1022 if parent_was != target_parent
1022 after_parent_changed(parent_was)
1023 after_parent_changed(parent_was)
1023 end
1024 end
1024 end
1025 end
1025 end
1026 end
@@ -1,698 +1,699
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 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 "digest/sha1"
18 require "digest/sha1"
19
19
20 class User < Principal
20 class User < Principal
21 include Redmine::SafeAttributes
21 include Redmine::SafeAttributes
22
22
23 # Different ways of displaying/sorting users
23 # Different ways of displaying/sorting users
24 USER_FORMATS = {
24 USER_FORMATS = {
25 :firstname_lastname => {
25 :firstname_lastname => {
26 :string => '#{firstname} #{lastname}',
26 :string => '#{firstname} #{lastname}',
27 :order => %w(firstname lastname id),
27 :order => %w(firstname lastname id),
28 :setting_order => 1
28 :setting_order => 1
29 },
29 },
30 :firstname_lastinitial => {
30 :firstname_lastinitial => {
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
31 :string => '#{firstname} #{lastname.to_s.chars.first}.',
32 :order => %w(firstname lastname id),
32 :order => %w(firstname lastname id),
33 :setting_order => 2
33 :setting_order => 2
34 },
34 },
35 :firstname => {
35 :firstname => {
36 :string => '#{firstname}',
36 :string => '#{firstname}',
37 :order => %w(firstname id),
37 :order => %w(firstname id),
38 :setting_order => 3
38 :setting_order => 3
39 },
39 },
40 :lastname_firstname => {
40 :lastname_firstname => {
41 :string => '#{lastname} #{firstname}',
41 :string => '#{lastname} #{firstname}',
42 :order => %w(lastname firstname id),
42 :order => %w(lastname firstname id),
43 :setting_order => 4
43 :setting_order => 4
44 },
44 },
45 :lastname_coma_firstname => {
45 :lastname_coma_firstname => {
46 :string => '#{lastname}, #{firstname}',
46 :string => '#{lastname}, #{firstname}',
47 :order => %w(lastname firstname id),
47 :order => %w(lastname firstname id),
48 :setting_order => 5
48 :setting_order => 5
49 },
49 },
50 :lastname => {
50 :lastname => {
51 :string => '#{lastname}',
51 :string => '#{lastname}',
52 :order => %w(lastname id),
52 :order => %w(lastname id),
53 :setting_order => 6
53 :setting_order => 6
54 },
54 },
55 :username => {
55 :username => {
56 :string => '#{login}',
56 :string => '#{login}',
57 :order => %w(login id),
57 :order => %w(login id),
58 :setting_order => 7
58 :setting_order => 7
59 },
59 },
60 }
60 }
61
61
62 MAIL_NOTIFICATION_OPTIONS = [
62 MAIL_NOTIFICATION_OPTIONS = [
63 ['all', :label_user_mail_option_all],
63 ['all', :label_user_mail_option_all],
64 ['selected', :label_user_mail_option_selected],
64 ['selected', :label_user_mail_option_selected],
65 ['only_my_events', :label_user_mail_option_only_my_events],
65 ['only_my_events', :label_user_mail_option_only_my_events],
66 ['only_assigned', :label_user_mail_option_only_assigned],
66 ['only_assigned', :label_user_mail_option_only_assigned],
67 ['only_owner', :label_user_mail_option_only_owner],
67 ['only_owner', :label_user_mail_option_only_owner],
68 ['none', :label_user_mail_option_none]
68 ['none', :label_user_mail_option_none]
69 ]
69 ]
70
70
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
71 has_and_belongs_to_many :groups, :after_add => Proc.new {|user, group| group.user_added(user)},
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
72 :after_remove => Proc.new {|user, group| group.user_removed(user)}
73 has_many :changesets, :dependent => :nullify
73 has_many :changesets, :dependent => :nullify
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
74 has_one :preference, :dependent => :destroy, :class_name => 'UserPreference'
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
75 has_one :rss_token, :class_name => 'Token', :conditions => "action='feeds'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
76 has_one :api_token, :class_name => 'Token', :conditions => "action='api'"
77 belongs_to :auth_source
77 belongs_to :auth_source
78
78
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
79 scope :logged, lambda { where("#{User.table_name}.status <> #{STATUS_ANONYMOUS}") }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
80 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
81
81
82 acts_as_customizable
82 acts_as_customizable
83
83
84 attr_accessor :password, :password_confirmation, :generate_password
84 attr_accessor :password, :password_confirmation, :generate_password
85 attr_accessor :last_before_login_on
85 attr_accessor :last_before_login_on
86 # Prevents unauthorized assignments
86 # Prevents unauthorized assignments
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
87 attr_protected :login, :admin, :password, :password_confirmation, :hashed_password
88
88
89 LOGIN_LENGTH_LIMIT = 60
89 LOGIN_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
90 MAIL_LENGTH_LIMIT = 60
91
91
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
92 validates_presence_of :login, :firstname, :lastname, :mail, :if => Proc.new { |user| !user.is_a?(AnonymousUser) }
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
93 validates_uniqueness_of :login, :if => Proc.new { |user| user.login_changed? && user.login.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
94 validates_uniqueness_of :mail, :if => Proc.new { |user| user.mail_changed? && user.mail.present? }, :case_sensitive => false
95 # Login must contain letters, numbers, underscores only
95 # Login must contain letters, numbers, underscores only
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
96 validates_format_of :login, :with => /\A[a-z0-9_\-@\.]*\z/i
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
97 validates_length_of :login, :maximum => LOGIN_LENGTH_LIMIT
98 validates_length_of :firstname, :lastname, :maximum => 30
98 validates_length_of :firstname, :lastname, :maximum => 30
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
99 validates_format_of :mail, :with => /\A([^@\s]+)@((?:[-a-z0-9]+\.)+[a-z]{2,})\z/i, :allow_blank => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
100 validates_length_of :mail, :maximum => MAIL_LENGTH_LIMIT, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
101 validates_confirmation_of :password, :allow_nil => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
102 validates_inclusion_of :mail_notification, :in => MAIL_NOTIFICATION_OPTIONS.collect(&:first), :allow_blank => true
103 validate :validate_password_length
103 validate :validate_password_length
104
104
105 before_create :set_mail_notification
105 before_create :set_mail_notification
106 before_save :generate_password_if_needed, :update_hashed_password
106 before_save :generate_password_if_needed, :update_hashed_password
107 before_destroy :remove_references_before_destroy
107 before_destroy :remove_references_before_destroy
108
108
109 scope :in_group, lambda {|group|
109 scope :in_group, lambda {|group|
110 group_id = group.is_a?(Group) ? group.id : group.to_i
110 group_id = group.is_a?(Group) ? group.id : group.to_i
111 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
111 where("#{User.table_name}.id IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
112 }
112 }
113 scope :not_in_group, lambda {|group|
113 scope :not_in_group, lambda {|group|
114 group_id = group.is_a?(Group) ? group.id : group.to_i
114 group_id = group.is_a?(Group) ? group.id : group.to_i
115 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
115 where("#{User.table_name}.id NOT IN (SELECT gu.user_id FROM #{table_name_prefix}groups_users#{table_name_suffix} gu WHERE gu.group_id = ?)", group_id)
116 }
116 }
117 scope :sorted, lambda { order(*User.fields_for_order_statement)}
117 scope :sorted, lambda { order(*User.fields_for_order_statement)}
118
118
119 def set_mail_notification
119 def set_mail_notification
120 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
120 self.mail_notification = Setting.default_notification_option if self.mail_notification.blank?
121 true
121 true
122 end
122 end
123
123
124 def update_hashed_password
124 def update_hashed_password
125 # update hashed_password if password was set
125 # update hashed_password if password was set
126 if self.password && self.auth_source_id.blank?
126 if self.password && self.auth_source_id.blank?
127 salt_password(password)
127 salt_password(password)
128 end
128 end
129 end
129 end
130
130
131 alias :base_reload :reload
131 def reload(*args)
132 def reload(*args)
132 @name = nil
133 @name = nil
133 @projects_by_role = nil
134 @projects_by_role = nil
134 super
135 base_reload(*args)
135 end
136 end
136
137
137 def mail=(arg)
138 def mail=(arg)
138 write_attribute(:mail, arg.to_s.strip)
139 write_attribute(:mail, arg.to_s.strip)
139 end
140 end
140
141
141 def identity_url=(url)
142 def identity_url=(url)
142 if url.blank?
143 if url.blank?
143 write_attribute(:identity_url, '')
144 write_attribute(:identity_url, '')
144 else
145 else
145 begin
146 begin
146 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
147 write_attribute(:identity_url, OpenIdAuthentication.normalize_identifier(url))
147 rescue OpenIdAuthentication::InvalidOpenId
148 rescue OpenIdAuthentication::InvalidOpenId
148 # Invalid url, don't save
149 # Invalid url, don't save
149 end
150 end
150 end
151 end
151 self.read_attribute(:identity_url)
152 self.read_attribute(:identity_url)
152 end
153 end
153
154
154 # Returns the user that matches provided login and password, or nil
155 # Returns the user that matches provided login and password, or nil
155 def self.try_to_login(login, password)
156 def self.try_to_login(login, password)
156 login = login.to_s
157 login = login.to_s
157 password = password.to_s
158 password = password.to_s
158
159
159 # Make sure no one can sign in with an empty login or password
160 # Make sure no one can sign in with an empty login or password
160 return nil if login.empty? || password.empty?
161 return nil if login.empty? || password.empty?
161 user = find_by_login(login)
162 user = find_by_login(login)
162 if user
163 if user
163 # user is already in local database
164 # user is already in local database
164 return nil unless user.active?
165 return nil unless user.active?
165 return nil unless user.check_password?(password)
166 return nil unless user.check_password?(password)
166 else
167 else
167 # user is not yet registered, try to authenticate with available sources
168 # user is not yet registered, try to authenticate with available sources
168 attrs = AuthSource.authenticate(login, password)
169 attrs = AuthSource.authenticate(login, password)
169 if attrs
170 if attrs
170 user = new(attrs)
171 user = new(attrs)
171 user.login = login
172 user.login = login
172 user.language = Setting.default_language
173 user.language = Setting.default_language
173 if user.save
174 if user.save
174 user.reload
175 user.reload
175 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
176 logger.info("User '#{user.login}' created from external auth source: #{user.auth_source.type} - #{user.auth_source.name}") if logger && user.auth_source
176 end
177 end
177 end
178 end
178 end
179 end
179 user.update_column(:last_login_on, Time.now) if user && !user.new_record?
180 user.update_column(:last_login_on, Time.now) if user && !user.new_record?
180 user
181 user
181 rescue => text
182 rescue => text
182 raise text
183 raise text
183 end
184 end
184
185
185 # Returns the user who matches the given autologin +key+ or nil
186 # Returns the user who matches the given autologin +key+ or nil
186 def self.try_to_autologin(key)
187 def self.try_to_autologin(key)
187 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
188 user = Token.find_active_user('autologin', key, Setting.autologin.to_i)
188 if user
189 if user
189 user.update_column(:last_login_on, Time.now)
190 user.update_column(:last_login_on, Time.now)
190 user
191 user
191 end
192 end
192 end
193 end
193
194
194 def self.name_formatter(formatter = nil)
195 def self.name_formatter(formatter = nil)
195 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
196 USER_FORMATS[formatter || Setting.user_format] || USER_FORMATS[:firstname_lastname]
196 end
197 end
197
198
198 # Returns an array of fields names than can be used to make an order statement for users
199 # Returns an array of fields names than can be used to make an order statement for users
199 # according to how user names are displayed
200 # according to how user names are displayed
200 # Examples:
201 # Examples:
201 #
202 #
202 # User.fields_for_order_statement => ['users.login', 'users.id']
203 # User.fields_for_order_statement => ['users.login', 'users.id']
203 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
204 # User.fields_for_order_statement('authors') => ['authors.login', 'authors.id']
204 def self.fields_for_order_statement(table=nil)
205 def self.fields_for_order_statement(table=nil)
205 table ||= table_name
206 table ||= table_name
206 name_formatter[:order].map {|field| "#{table}.#{field}"}
207 name_formatter[:order].map {|field| "#{table}.#{field}"}
207 end
208 end
208
209
209 # Return user's full name for display
210 # Return user's full name for display
210 def name(formatter = nil)
211 def name(formatter = nil)
211 f = self.class.name_formatter(formatter)
212 f = self.class.name_formatter(formatter)
212 if formatter
213 if formatter
213 eval('"' + f[:string] + '"')
214 eval('"' + f[:string] + '"')
214 else
215 else
215 @name ||= eval('"' + f[:string] + '"')
216 @name ||= eval('"' + f[:string] + '"')
216 end
217 end
217 end
218 end
218
219
219 def active?
220 def active?
220 self.status == STATUS_ACTIVE
221 self.status == STATUS_ACTIVE
221 end
222 end
222
223
223 def registered?
224 def registered?
224 self.status == STATUS_REGISTERED
225 self.status == STATUS_REGISTERED
225 end
226 end
226
227
227 def locked?
228 def locked?
228 self.status == STATUS_LOCKED
229 self.status == STATUS_LOCKED
229 end
230 end
230
231
231 def activate
232 def activate
232 self.status = STATUS_ACTIVE
233 self.status = STATUS_ACTIVE
233 end
234 end
234
235
235 def register
236 def register
236 self.status = STATUS_REGISTERED
237 self.status = STATUS_REGISTERED
237 end
238 end
238
239
239 def lock
240 def lock
240 self.status = STATUS_LOCKED
241 self.status = STATUS_LOCKED
241 end
242 end
242
243
243 def activate!
244 def activate!
244 update_attribute(:status, STATUS_ACTIVE)
245 update_attribute(:status, STATUS_ACTIVE)
245 end
246 end
246
247
247 def register!
248 def register!
248 update_attribute(:status, STATUS_REGISTERED)
249 update_attribute(:status, STATUS_REGISTERED)
249 end
250 end
250
251
251 def lock!
252 def lock!
252 update_attribute(:status, STATUS_LOCKED)
253 update_attribute(:status, STATUS_LOCKED)
253 end
254 end
254
255
255 # Returns true if +clear_password+ is the correct user's password, otherwise false
256 # Returns true if +clear_password+ is the correct user's password, otherwise false
256 def check_password?(clear_password)
257 def check_password?(clear_password)
257 if auth_source_id.present?
258 if auth_source_id.present?
258 auth_source.authenticate(self.login, clear_password)
259 auth_source.authenticate(self.login, clear_password)
259 else
260 else
260 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
261 User.hash_password("#{salt}#{User.hash_password clear_password}") == hashed_password
261 end
262 end
262 end
263 end
263
264
264 # Generates a random salt and computes hashed_password for +clear_password+
265 # Generates a random salt and computes hashed_password for +clear_password+
265 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
266 # The hashed password is stored in the following form: SHA1(salt + SHA1(password))
266 def salt_password(clear_password)
267 def salt_password(clear_password)
267 self.salt = User.generate_salt
268 self.salt = User.generate_salt
268 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
269 self.hashed_password = User.hash_password("#{salt}#{User.hash_password clear_password}")
269 end
270 end
270
271
271 # Does the backend storage allow this user to change their password?
272 # Does the backend storage allow this user to change their password?
272 def change_password_allowed?
273 def change_password_allowed?
273 return true if auth_source.nil?
274 return true if auth_source.nil?
274 return auth_source.allow_password_changes?
275 return auth_source.allow_password_changes?
275 end
276 end
276
277
277 def generate_password?
278 def generate_password?
278 generate_password == '1' || generate_password == true
279 generate_password == '1' || generate_password == true
279 end
280 end
280
281
281 # Generate and set a random password on given length
282 # Generate and set a random password on given length
282 def random_password(length=40)
283 def random_password(length=40)
283 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
284 chars = ("a".."z").to_a + ("A".."Z").to_a + ("0".."9").to_a
284 chars -= %w(0 O 1 l)
285 chars -= %w(0 O 1 l)
285 password = ''
286 password = ''
286 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
287 length.times {|i| password << chars[SecureRandom.random_number(chars.size)] }
287 self.password = password
288 self.password = password
288 self.password_confirmation = password
289 self.password_confirmation = password
289 self
290 self
290 end
291 end
291
292
292 def pref
293 def pref
293 self.preference ||= UserPreference.new(:user => self)
294 self.preference ||= UserPreference.new(:user => self)
294 end
295 end
295
296
296 def time_zone
297 def time_zone
297 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
298 @time_zone ||= (self.pref.time_zone.blank? ? nil : ActiveSupport::TimeZone[self.pref.time_zone])
298 end
299 end
299
300
300 def wants_comments_in_reverse_order?
301 def wants_comments_in_reverse_order?
301 self.pref[:comments_sorting] == 'desc'
302 self.pref[:comments_sorting] == 'desc'
302 end
303 end
303
304
304 # Return user's RSS key (a 40 chars long string), used to access feeds
305 # Return user's RSS key (a 40 chars long string), used to access feeds
305 def rss_key
306 def rss_key
306 if rss_token.nil?
307 if rss_token.nil?
307 create_rss_token(:action => 'feeds')
308 create_rss_token(:action => 'feeds')
308 end
309 end
309 rss_token.value
310 rss_token.value
310 end
311 end
311
312
312 # Return user's API key (a 40 chars long string), used to access the API
313 # Return user's API key (a 40 chars long string), used to access the API
313 def api_key
314 def api_key
314 if api_token.nil?
315 if api_token.nil?
315 create_api_token(:action => 'api')
316 create_api_token(:action => 'api')
316 end
317 end
317 api_token.value
318 api_token.value
318 end
319 end
319
320
320 # Return an array of project ids for which the user has explicitly turned mail notifications on
321 # Return an array of project ids for which the user has explicitly turned mail notifications on
321 def notified_projects_ids
322 def notified_projects_ids
322 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
323 @notified_projects_ids ||= memberships.select {|m| m.mail_notification?}.collect(&:project_id)
323 end
324 end
324
325
325 def notified_project_ids=(ids)
326 def notified_project_ids=(ids)
326 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
327 Member.update_all("mail_notification = #{connection.quoted_false}", ['user_id = ?', id])
327 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
328 Member.update_all("mail_notification = #{connection.quoted_true}", ['user_id = ? AND project_id IN (?)', id, ids]) if ids && !ids.empty?
328 @notified_projects_ids = nil
329 @notified_projects_ids = nil
329 notified_projects_ids
330 notified_projects_ids
330 end
331 end
331
332
332 def valid_notification_options
333 def valid_notification_options
333 self.class.valid_notification_options(self)
334 self.class.valid_notification_options(self)
334 end
335 end
335
336
336 # Only users that belong to more than 1 project can select projects for which they are notified
337 # Only users that belong to more than 1 project can select projects for which they are notified
337 def self.valid_notification_options(user=nil)
338 def self.valid_notification_options(user=nil)
338 # Note that @user.membership.size would fail since AR ignores
339 # Note that @user.membership.size would fail since AR ignores
339 # :include association option when doing a count
340 # :include association option when doing a count
340 if user.nil? || user.memberships.length < 1
341 if user.nil? || user.memberships.length < 1
341 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
342 MAIL_NOTIFICATION_OPTIONS.reject {|option| option.first == 'selected'}
342 else
343 else
343 MAIL_NOTIFICATION_OPTIONS
344 MAIL_NOTIFICATION_OPTIONS
344 end
345 end
345 end
346 end
346
347
347 # Find a user account by matching the exact login and then a case-insensitive
348 # Find a user account by matching the exact login and then a case-insensitive
348 # version. Exact matches will be given priority.
349 # version. Exact matches will be given priority.
349 def self.find_by_login(login)
350 def self.find_by_login(login)
350 if login.present?
351 if login.present?
351 login = login.to_s
352 login = login.to_s
352 # First look for an exact match
353 # First look for an exact match
353 user = where(:login => login).all.detect {|u| u.login == login}
354 user = where(:login => login).all.detect {|u| u.login == login}
354 unless user
355 unless user
355 # Fail over to case-insensitive if none was found
356 # Fail over to case-insensitive if none was found
356 user = where("LOWER(login) = ?", login.downcase).first
357 user = where("LOWER(login) = ?", login.downcase).first
357 end
358 end
358 user
359 user
359 end
360 end
360 end
361 end
361
362
362 def self.find_by_rss_key(key)
363 def self.find_by_rss_key(key)
363 Token.find_active_user('feeds', key)
364 Token.find_active_user('feeds', key)
364 end
365 end
365
366
366 def self.find_by_api_key(key)
367 def self.find_by_api_key(key)
367 Token.find_active_user('api', key)
368 Token.find_active_user('api', key)
368 end
369 end
369
370
370 # Makes find_by_mail case-insensitive
371 # Makes find_by_mail case-insensitive
371 def self.find_by_mail(mail)
372 def self.find_by_mail(mail)
372 where("LOWER(mail) = ?", mail.to_s.downcase).first
373 where("LOWER(mail) = ?", mail.to_s.downcase).first
373 end
374 end
374
375
375 # Returns true if the default admin account can no longer be used
376 # Returns true if the default admin account can no longer be used
376 def self.default_admin_account_changed?
377 def self.default_admin_account_changed?
377 !User.active.find_by_login("admin").try(:check_password?, "admin")
378 !User.active.find_by_login("admin").try(:check_password?, "admin")
378 end
379 end
379
380
380 def to_s
381 def to_s
381 name
382 name
382 end
383 end
383
384
384 CSS_CLASS_BY_STATUS = {
385 CSS_CLASS_BY_STATUS = {
385 STATUS_ANONYMOUS => 'anon',
386 STATUS_ANONYMOUS => 'anon',
386 STATUS_ACTIVE => 'active',
387 STATUS_ACTIVE => 'active',
387 STATUS_REGISTERED => 'registered',
388 STATUS_REGISTERED => 'registered',
388 STATUS_LOCKED => 'locked'
389 STATUS_LOCKED => 'locked'
389 }
390 }
390
391
391 def css_classes
392 def css_classes
392 "user #{CSS_CLASS_BY_STATUS[status]}"
393 "user #{CSS_CLASS_BY_STATUS[status]}"
393 end
394 end
394
395
395 # Returns the current day according to user's time zone
396 # Returns the current day according to user's time zone
396 def today
397 def today
397 if time_zone.nil?
398 if time_zone.nil?
398 Date.today
399 Date.today
399 else
400 else
400 Time.now.in_time_zone(time_zone).to_date
401 Time.now.in_time_zone(time_zone).to_date
401 end
402 end
402 end
403 end
403
404
404 # Returns the day of +time+ according to user's time zone
405 # Returns the day of +time+ according to user's time zone
405 def time_to_date(time)
406 def time_to_date(time)
406 if time_zone.nil?
407 if time_zone.nil?
407 time.to_date
408 time.to_date
408 else
409 else
409 time.in_time_zone(time_zone).to_date
410 time.in_time_zone(time_zone).to_date
410 end
411 end
411 end
412 end
412
413
413 def logged?
414 def logged?
414 true
415 true
415 end
416 end
416
417
417 def anonymous?
418 def anonymous?
418 !logged?
419 !logged?
419 end
420 end
420
421
421 # Return user's roles for project
422 # Return user's roles for project
422 def roles_for_project(project)
423 def roles_for_project(project)
423 roles = []
424 roles = []
424 # No role on archived projects
425 # No role on archived projects
425 return roles if project.nil? || project.archived?
426 return roles if project.nil? || project.archived?
426 if logged?
427 if logged?
427 # Find project membership
428 # Find project membership
428 membership = memberships.detect {|m| m.project_id == project.id}
429 membership = memberships.detect {|m| m.project_id == project.id}
429 if membership
430 if membership
430 roles = membership.roles
431 roles = membership.roles
431 else
432 else
432 @role_non_member ||= Role.non_member
433 @role_non_member ||= Role.non_member
433 roles << @role_non_member
434 roles << @role_non_member
434 end
435 end
435 else
436 else
436 @role_anonymous ||= Role.anonymous
437 @role_anonymous ||= Role.anonymous
437 roles << @role_anonymous
438 roles << @role_anonymous
438 end
439 end
439 roles
440 roles
440 end
441 end
441
442
442 # Return true if the user is a member of project
443 # Return true if the user is a member of project
443 def member_of?(project)
444 def member_of?(project)
444 roles_for_project(project).any? {|role| role.member?}
445 roles_for_project(project).any? {|role| role.member?}
445 end
446 end
446
447
447 # Returns a hash of user's projects grouped by roles
448 # Returns a hash of user's projects grouped by roles
448 def projects_by_role
449 def projects_by_role
449 return @projects_by_role if @projects_by_role
450 return @projects_by_role if @projects_by_role
450
451
451 @projects_by_role = Hash.new([])
452 @projects_by_role = Hash.new([])
452 memberships.each do |membership|
453 memberships.each do |membership|
453 if membership.project
454 if membership.project
454 membership.roles.each do |role|
455 membership.roles.each do |role|
455 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
456 @projects_by_role[role] = [] unless @projects_by_role.key?(role)
456 @projects_by_role[role] << membership.project
457 @projects_by_role[role] << membership.project
457 end
458 end
458 end
459 end
459 end
460 end
460 @projects_by_role.each do |role, projects|
461 @projects_by_role.each do |role, projects|
461 projects.uniq!
462 projects.uniq!
462 end
463 end
463
464
464 @projects_by_role
465 @projects_by_role
465 end
466 end
466
467
467 # Returns true if user is arg or belongs to arg
468 # Returns true if user is arg or belongs to arg
468 def is_or_belongs_to?(arg)
469 def is_or_belongs_to?(arg)
469 if arg.is_a?(User)
470 if arg.is_a?(User)
470 self == arg
471 self == arg
471 elsif arg.is_a?(Group)
472 elsif arg.is_a?(Group)
472 arg.users.include?(self)
473 arg.users.include?(self)
473 else
474 else
474 false
475 false
475 end
476 end
476 end
477 end
477
478
478 # Return true if the user is allowed to do the specified action on a specific context
479 # Return true if the user is allowed to do the specified action on a specific context
479 # Action can be:
480 # Action can be:
480 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
481 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
481 # * a permission Symbol (eg. :edit_project)
482 # * a permission Symbol (eg. :edit_project)
482 # Context can be:
483 # Context can be:
483 # * a project : returns true if user is allowed to do the specified action on this project
484 # * a project : returns true if user is allowed to do the specified action on this project
484 # * an array of projects : returns true if user is allowed on every project
485 # * an array of projects : returns true if user is allowed on every project
485 # * nil with options[:global] set : check if user has at least one role allowed for this action,
486 # * nil with options[:global] set : check if user has at least one role allowed for this action,
486 # or falls back to Non Member / Anonymous permissions depending if the user is logged
487 # or falls back to Non Member / Anonymous permissions depending if the user is logged
487 def allowed_to?(action, context, options={}, &block)
488 def allowed_to?(action, context, options={}, &block)
488 if context && context.is_a?(Project)
489 if context && context.is_a?(Project)
489 return false unless context.allows_to?(action)
490 return false unless context.allows_to?(action)
490 # Admin users are authorized for anything else
491 # Admin users are authorized for anything else
491 return true if admin?
492 return true if admin?
492
493
493 roles = roles_for_project(context)
494 roles = roles_for_project(context)
494 return false unless roles
495 return false unless roles
495 roles.any? {|role|
496 roles.any? {|role|
496 (context.is_public? || role.member?) &&
497 (context.is_public? || role.member?) &&
497 role.allowed_to?(action) &&
498 role.allowed_to?(action) &&
498 (block_given? ? yield(role, self) : true)
499 (block_given? ? yield(role, self) : true)
499 }
500 }
500 elsif context && context.is_a?(Array)
501 elsif context && context.is_a?(Array)
501 if context.empty?
502 if context.empty?
502 false
503 false
503 else
504 else
504 # Authorize if user is authorized on every element of the array
505 # Authorize if user is authorized on every element of the array
505 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
506 context.map {|project| allowed_to?(action, project, options, &block)}.reduce(:&)
506 end
507 end
507 elsif options[:global]
508 elsif options[:global]
508 # Admin users are always authorized
509 # Admin users are always authorized
509 return true if admin?
510 return true if admin?
510
511
511 # authorize if user has at least one role that has this permission
512 # authorize if user has at least one role that has this permission
512 roles = memberships.collect {|m| m.roles}.flatten.uniq
513 roles = memberships.collect {|m| m.roles}.flatten.uniq
513 roles << (self.logged? ? Role.non_member : Role.anonymous)
514 roles << (self.logged? ? Role.non_member : Role.anonymous)
514 roles.any? {|role|
515 roles.any? {|role|
515 role.allowed_to?(action) &&
516 role.allowed_to?(action) &&
516 (block_given? ? yield(role, self) : true)
517 (block_given? ? yield(role, self) : true)
517 }
518 }
518 else
519 else
519 false
520 false
520 end
521 end
521 end
522 end
522
523
523 # Is the user allowed to do the specified action on any project?
524 # Is the user allowed to do the specified action on any project?
524 # See allowed_to? for the actions and valid options.
525 # See allowed_to? for the actions and valid options.
525 def allowed_to_globally?(action, options, &block)
526 def allowed_to_globally?(action, options, &block)
526 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
527 allowed_to?(action, nil, options.reverse_merge(:global => true), &block)
527 end
528 end
528
529
529 # Returns true if the user is allowed to delete his own account
530 # Returns true if the user is allowed to delete his own account
530 def own_account_deletable?
531 def own_account_deletable?
531 Setting.unsubscribe? &&
532 Setting.unsubscribe? &&
532 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
533 (!admin? || User.active.where("admin = ? AND id <> ?", true, id).exists?)
533 end
534 end
534
535
535 safe_attributes 'login',
536 safe_attributes 'login',
536 'firstname',
537 'firstname',
537 'lastname',
538 'lastname',
538 'mail',
539 'mail',
539 'mail_notification',
540 'mail_notification',
540 'language',
541 'language',
541 'custom_field_values',
542 'custom_field_values',
542 'custom_fields',
543 'custom_fields',
543 'identity_url'
544 'identity_url'
544
545
545 safe_attributes 'status',
546 safe_attributes 'status',
546 'auth_source_id',
547 'auth_source_id',
547 'generate_password',
548 'generate_password',
548 :if => lambda {|user, current_user| current_user.admin?}
549 :if => lambda {|user, current_user| current_user.admin?}
549
550
550 safe_attributes 'group_ids',
551 safe_attributes 'group_ids',
551 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
552 :if => lambda {|user, current_user| current_user.admin? && !user.new_record?}
552
553
553 # Utility method to help check if a user should be notified about an
554 # Utility method to help check if a user should be notified about an
554 # event.
555 # event.
555 #
556 #
556 # TODO: only supports Issue events currently
557 # TODO: only supports Issue events currently
557 def notify_about?(object)
558 def notify_about?(object)
558 if mail_notification == 'all'
559 if mail_notification == 'all'
559 true
560 true
560 elsif mail_notification.blank? || mail_notification == 'none'
561 elsif mail_notification.blank? || mail_notification == 'none'
561 false
562 false
562 else
563 else
563 case object
564 case object
564 when Issue
565 when Issue
565 case mail_notification
566 case mail_notification
566 when 'selected', 'only_my_events'
567 when 'selected', 'only_my_events'
567 # user receives notifications for created/assigned issues on unselected projects
568 # user receives notifications for created/assigned issues on unselected projects
568 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
569 object.author == self || is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
569 when 'only_assigned'
570 when 'only_assigned'
570 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
571 is_or_belongs_to?(object.assigned_to) || is_or_belongs_to?(object.assigned_to_was)
571 when 'only_owner'
572 when 'only_owner'
572 object.author == self
573 object.author == self
573 end
574 end
574 when News
575 when News
575 # always send to project members except when mail_notification is set to 'none'
576 # always send to project members except when mail_notification is set to 'none'
576 true
577 true
577 end
578 end
578 end
579 end
579 end
580 end
580
581
581 def self.current=(user)
582 def self.current=(user)
582 Thread.current[:current_user] = user
583 Thread.current[:current_user] = user
583 end
584 end
584
585
585 def self.current
586 def self.current
586 Thread.current[:current_user] ||= User.anonymous
587 Thread.current[:current_user] ||= User.anonymous
587 end
588 end
588
589
589 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
590 # Returns the anonymous user. If the anonymous user does not exist, it is created. There can be only
590 # one anonymous user per database.
591 # one anonymous user per database.
591 def self.anonymous
592 def self.anonymous
592 anonymous_user = AnonymousUser.first
593 anonymous_user = AnonymousUser.first
593 if anonymous_user.nil?
594 if anonymous_user.nil?
594 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
595 anonymous_user = AnonymousUser.create(:lastname => 'Anonymous', :firstname => '', :mail => '', :login => '', :status => 0)
595 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
596 raise 'Unable to create the anonymous user.' if anonymous_user.new_record?
596 end
597 end
597 anonymous_user
598 anonymous_user
598 end
599 end
599
600
600 # Salts all existing unsalted passwords
601 # Salts all existing unsalted passwords
601 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
602 # It changes password storage scheme from SHA1(password) to SHA1(salt + SHA1(password))
602 # This method is used in the SaltPasswords migration and is to be kept as is
603 # This method is used in the SaltPasswords migration and is to be kept as is
603 def self.salt_unsalted_passwords!
604 def self.salt_unsalted_passwords!
604 transaction do
605 transaction do
605 User.where("salt IS NULL OR salt = ''").find_each do |user|
606 User.where("salt IS NULL OR salt = ''").find_each do |user|
606 next if user.hashed_password.blank?
607 next if user.hashed_password.blank?
607 salt = User.generate_salt
608 salt = User.generate_salt
608 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
609 hashed_password = User.hash_password("#{salt}#{user.hashed_password}")
609 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
610 User.where(:id => user.id).update_all(:salt => salt, :hashed_password => hashed_password)
610 end
611 end
611 end
612 end
612 end
613 end
613
614
614 protected
615 protected
615
616
616 def validate_password_length
617 def validate_password_length
617 return if password.blank? && generate_password?
618 return if password.blank? && generate_password?
618 # Password length validation based on setting
619 # Password length validation based on setting
619 if !password.nil? && password.size < Setting.password_min_length.to_i
620 if !password.nil? && password.size < Setting.password_min_length.to_i
620 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
621 errors.add(:password, :too_short, :count => Setting.password_min_length.to_i)
621 end
622 end
622 end
623 end
623
624
624 private
625 private
625
626
626 def generate_password_if_needed
627 def generate_password_if_needed
627 if generate_password? && auth_source.nil?
628 if generate_password? && auth_source.nil?
628 length = [Setting.password_min_length.to_i + 2, 10].max
629 length = [Setting.password_min_length.to_i + 2, 10].max
629 random_password(length)
630 random_password(length)
630 end
631 end
631 end
632 end
632
633
633 # Removes references that are not handled by associations
634 # Removes references that are not handled by associations
634 # Things that are not deleted are reassociated with the anonymous user
635 # Things that are not deleted are reassociated with the anonymous user
635 def remove_references_before_destroy
636 def remove_references_before_destroy
636 return if self.id.nil?
637 return if self.id.nil?
637
638
638 substitute = User.anonymous
639 substitute = User.anonymous
639 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
640 Attachment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
640 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
641 Comment.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
641 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
642 Issue.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
642 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
643 Issue.update_all 'assigned_to_id = NULL', ['assigned_to_id = ?', id]
643 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
644 Journal.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
644 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
645 JournalDetail.update_all ['old_value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND old_value = ?", id.to_s]
645 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
646 JournalDetail.update_all ['value = ?', substitute.id.to_s], ["property = 'attr' AND prop_key = 'assigned_to_id' AND value = ?", id.to_s]
646 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
647 Message.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
647 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
648 News.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
648 # Remove private queries and keep public ones
649 # Remove private queries and keep public ones
649 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
650 ::Query.delete_all ['user_id = ? AND is_public = ?', id, false]
650 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
651 ::Query.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
651 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
652 TimeEntry.update_all ['user_id = ?', substitute.id], ['user_id = ?', id]
652 Token.delete_all ['user_id = ?', id]
653 Token.delete_all ['user_id = ?', id]
653 Watcher.delete_all ['user_id = ?', id]
654 Watcher.delete_all ['user_id = ?', id]
654 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
655 WikiContent.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
655 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
656 WikiContent::Version.update_all ['author_id = ?', substitute.id], ['author_id = ?', id]
656 end
657 end
657
658
658 # Return password digest
659 # Return password digest
659 def self.hash_password(clear_password)
660 def self.hash_password(clear_password)
660 Digest::SHA1.hexdigest(clear_password || "")
661 Digest::SHA1.hexdigest(clear_password || "")
661 end
662 end
662
663
663 # Returns a 128bits random salt as a hex string (32 chars long)
664 # Returns a 128bits random salt as a hex string (32 chars long)
664 def self.generate_salt
665 def self.generate_salt
665 Redmine::Utils.random_hex(16)
666 Redmine::Utils.random_hex(16)
666 end
667 end
667
668
668 end
669 end
669
670
670 class AnonymousUser < User
671 class AnonymousUser < User
671 validate :validate_anonymous_uniqueness, :on => :create
672 validate :validate_anonymous_uniqueness, :on => :create
672
673
673 def validate_anonymous_uniqueness
674 def validate_anonymous_uniqueness
674 # There should be only one AnonymousUser in the database
675 # There should be only one AnonymousUser in the database
675 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
676 errors.add :base, 'An anonymous user already exists.' if AnonymousUser.exists?
676 end
677 end
677
678
678 def available_custom_fields
679 def available_custom_fields
679 []
680 []
680 end
681 end
681
682
682 # Overrides a few properties
683 # Overrides a few properties
683 def logged?; false end
684 def logged?; false end
684 def admin; false end
685 def admin; false end
685 def name(*args); I18n.t(:label_user_anonymous) end
686 def name(*args); I18n.t(:label_user_anonymous) end
686 def mail; nil end
687 def mail; nil end
687 def time_zone; nil end
688 def time_zone; nil end
688 def rss_key; nil end
689 def rss_key; nil end
689
690
690 def pref
691 def pref
691 UserPreference.new(:user => self)
692 UserPreference.new(:user => self)
692 end
693 end
693
694
694 # Anonymous user can not be destroyed
695 # Anonymous user can not be destroyed
695 def destroy
696 def destroy
696 false
697 false
697 end
698 end
698 end
699 end
@@ -1,168 +1,168
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 module Redmine
18 module Redmine
19 module Acts
19 module Acts
20 module Customizable
20 module Customizable
21 def self.included(base)
21 def self.included(base)
22 base.extend ClassMethods
22 base.extend ClassMethods
23 end
23 end
24
24
25 module ClassMethods
25 module ClassMethods
26 def acts_as_customizable(options = {})
26 def acts_as_customizable(options = {})
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
27 return if self.included_modules.include?(Redmine::Acts::Customizable::InstanceMethods)
28 cattr_accessor :customizable_options
28 cattr_accessor :customizable_options
29 self.customizable_options = options
29 self.customizable_options = options
30 has_many :custom_values, :as => :customized,
30 has_many :custom_values, :as => :customized,
31 :include => :custom_field,
31 :include => :custom_field,
32 :order => "#{CustomField.table_name}.position",
32 :order => "#{CustomField.table_name}.position",
33 :dependent => :delete_all,
33 :dependent => :delete_all,
34 :validate => false
34 :validate => false
35
35
36 send :alias_method, :reload_without_custom_fields, :reload
37 send :include, Redmine::Acts::Customizable::InstanceMethods
36 send :include, Redmine::Acts::Customizable::InstanceMethods
38 validate :validate_custom_field_values
37 validate :validate_custom_field_values
39 after_save :save_custom_field_values
38 after_save :save_custom_field_values
40 end
39 end
41 end
40 end
42
41
43 module InstanceMethods
42 module InstanceMethods
44 def self.included(base)
43 def self.included(base)
45 base.extend ClassMethods
44 base.extend ClassMethods
45 base.send :alias_method_chain, :reload, :custom_fields
46 end
46 end
47
47
48 def available_custom_fields
48 def available_custom_fields
49 CustomField.where("type = '#{self.class.name}CustomField'").sorted.all
49 CustomField.where("type = '#{self.class.name}CustomField'").sorted.all
50 end
50 end
51
51
52 # Sets the values of the object's custom fields
52 # Sets the values of the object's custom fields
53 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
53 # values is an array like [{'id' => 1, 'value' => 'foo'}, {'id' => 2, 'value' => 'bar'}]
54 def custom_fields=(values)
54 def custom_fields=(values)
55 values_to_hash = values.inject({}) do |hash, v|
55 values_to_hash = values.inject({}) do |hash, v|
56 v = v.stringify_keys
56 v = v.stringify_keys
57 if v['id'] && v.has_key?('value')
57 if v['id'] && v.has_key?('value')
58 hash[v['id']] = v['value']
58 hash[v['id']] = v['value']
59 end
59 end
60 hash
60 hash
61 end
61 end
62 self.custom_field_values = values_to_hash
62 self.custom_field_values = values_to_hash
63 end
63 end
64
64
65 # Sets the values of the object's custom fields
65 # Sets the values of the object's custom fields
66 # values is a hash like {'1' => 'foo', 2 => 'bar'}
66 # values is a hash like {'1' => 'foo', 2 => 'bar'}
67 def custom_field_values=(values)
67 def custom_field_values=(values)
68 values = values.stringify_keys
68 values = values.stringify_keys
69
69
70 custom_field_values.each do |custom_field_value|
70 custom_field_values.each do |custom_field_value|
71 key = custom_field_value.custom_field_id.to_s
71 key = custom_field_value.custom_field_id.to_s
72 if values.has_key?(key)
72 if values.has_key?(key)
73 value = values[key]
73 value = values[key]
74 if value.is_a?(Array)
74 if value.is_a?(Array)
75 value = value.reject(&:blank?).uniq
75 value = value.reject(&:blank?).uniq
76 if value.empty?
76 if value.empty?
77 value << ''
77 value << ''
78 end
78 end
79 end
79 end
80 custom_field_value.value = value
80 custom_field_value.value = value
81 end
81 end
82 end
82 end
83 @custom_field_values_changed = true
83 @custom_field_values_changed = true
84 end
84 end
85
85
86 def custom_field_values
86 def custom_field_values
87 @custom_field_values ||= available_custom_fields.collect do |field|
87 @custom_field_values ||= available_custom_fields.collect do |field|
88 x = CustomFieldValue.new
88 x = CustomFieldValue.new
89 x.custom_field = field
89 x.custom_field = field
90 x.customized = self
90 x.customized = self
91 if field.multiple?
91 if field.multiple?
92 values = custom_values.select { |v| v.custom_field == field }
92 values = custom_values.select { |v| v.custom_field == field }
93 if values.empty?
93 if values.empty?
94 values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
94 values << custom_values.build(:customized => self, :custom_field => field, :value => nil)
95 end
95 end
96 x.value = values.map(&:value)
96 x.value = values.map(&:value)
97 else
97 else
98 cv = custom_values.detect { |v| v.custom_field == field }
98 cv = custom_values.detect { |v| v.custom_field == field }
99 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
99 cv ||= custom_values.build(:customized => self, :custom_field => field, :value => nil)
100 x.value = cv.value
100 x.value = cv.value
101 end
101 end
102 x
102 x
103 end
103 end
104 end
104 end
105
105
106 def visible_custom_field_values
106 def visible_custom_field_values
107 custom_field_values.select(&:visible?)
107 custom_field_values.select(&:visible?)
108 end
108 end
109
109
110 def custom_field_values_changed?
110 def custom_field_values_changed?
111 @custom_field_values_changed == true
111 @custom_field_values_changed == true
112 end
112 end
113
113
114 def custom_value_for(c)
114 def custom_value_for(c)
115 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
115 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
116 custom_values.detect {|v| v.custom_field_id == field_id }
116 custom_values.detect {|v| v.custom_field_id == field_id }
117 end
117 end
118
118
119 def custom_field_value(c)
119 def custom_field_value(c)
120 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
120 field_id = (c.is_a?(CustomField) ? c.id : c.to_i)
121 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
121 custom_field_values.detect {|v| v.custom_field_id == field_id }.try(:value)
122 end
122 end
123
123
124 def validate_custom_field_values
124 def validate_custom_field_values
125 if new_record? || custom_field_values_changed?
125 if new_record? || custom_field_values_changed?
126 custom_field_values.each(&:validate_value)
126 custom_field_values.each(&:validate_value)
127 end
127 end
128 end
128 end
129
129
130 def save_custom_field_values
130 def save_custom_field_values
131 target_custom_values = []
131 target_custom_values = []
132 custom_field_values.each do |custom_field_value|
132 custom_field_values.each do |custom_field_value|
133 if custom_field_value.value.is_a?(Array)
133 if custom_field_value.value.is_a?(Array)
134 custom_field_value.value.each do |v|
134 custom_field_value.value.each do |v|
135 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
135 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field && cv.value == v}
136 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
136 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field, :value => v)
137 target_custom_values << target
137 target_custom_values << target
138 end
138 end
139 else
139 else
140 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
140 target = custom_values.detect {|cv| cv.custom_field == custom_field_value.custom_field}
141 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
141 target ||= custom_values.build(:customized => self, :custom_field => custom_field_value.custom_field)
142 target.value = custom_field_value.value
142 target.value = custom_field_value.value
143 target_custom_values << target
143 target_custom_values << target
144 end
144 end
145 end
145 end
146 self.custom_values = target_custom_values
146 self.custom_values = target_custom_values
147 custom_values.each(&:save)
147 custom_values.each(&:save)
148 @custom_field_values_changed = false
148 @custom_field_values_changed = false
149 true
149 true
150 end
150 end
151
151
152 def reset_custom_values!
152 def reset_custom_values!
153 @custom_field_values = nil
153 @custom_field_values = nil
154 @custom_field_values_changed = true
154 @custom_field_values_changed = true
155 end
155 end
156
156
157 def reload(*args)
157 def reload_with_custom_fields(*args)
158 @custom_field_values = nil
158 @custom_field_values = nil
159 @custom_field_values_changed = false
159 @custom_field_values_changed = false
160 reload_without_custom_fields(*args)
160 reload_without_custom_fields(*args)
161 end
161 end
162
162
163 module ClassMethods
163 module ClassMethods
164 end
164 end
165 end
165 end
166 end
166 end
167 end
167 end
168 end
168 end
General Comments 0
You need to be logged in to leave comments. Login now