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