comparison app/models/project.rb @ 1298:4f746d8966dd redmine_2.3_integration

Merge from redmine-2.3 branch to create new branch redmine-2.3-integration
author Chris Cannam
date Fri, 14 Jun 2013 09:28:30 +0100
parents 0a574315af3e 622f24f53b42
children
comparison
equal deleted inserted replaced
1297:0a574315af3e 1298:4f746d8966dd
1 # Redmine - project management software 1 # Redmine - project management software
2 # Copyright (C) 2006-2012 Jean-Philippe Lang 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 # 3 #
4 # This program is free software; you can redistribute it and/or 4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License 5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2 6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version. 7 # of the License, or (at your option) any later version.
26 # Maximum length for project identifiers 26 # Maximum length for project identifiers
27 IDENTIFIER_MAX_LENGTH = 100 27 IDENTIFIER_MAX_LENGTH = 100
28 28
29 # Specific overidden Activities 29 # Specific overidden Activities
30 has_many :time_entry_activities 30 has_many :time_entry_activities
31 has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" 31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 has_many :memberships, :class_name => 'Member' 32 has_many :memberships, :class_name => 'Member'
33 has_many :member_principals, :class_name => 'Member', 33 has_many :member_principals, :class_name => 'Member',
34 :include => :principal, 34 :include => :principal,
35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})" 35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 has_many :users, :through => :members 36 has_many :users, :through => :members
37 has_many :principals, :through => :member_principals, :source => :principal 37 has_many :principals, :through => :member_principals, :source => :principal
38 38
39 has_many :enabled_modules, :dependent => :delete_all 39 has_many :enabled_modules, :dependent => :delete_all
40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" 40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker] 41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 has_many :issue_changes, :through => :issues, :source => :journals 42 has_many :issue_changes, :through => :issues, :source => :journals
43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" 43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 has_many :time_entries, :dependent => :delete_all 44 has_many :time_entries, :dependent => :delete_all
45 has_many :queries, :dependent => :delete_all 45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
46 has_many :documents, :dependent => :destroy 46 has_many :documents, :dependent => :destroy
47 has_many :news, :dependent => :destroy, :include => :author 47 has_many :news, :dependent => :destroy, :include => :author
48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" 48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 has_many :boards, :dependent => :destroy, :order => "position ASC" 49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 has_one :repository, :conditions => ["is_default = ?", true] 50 has_one :repository, :conditions => ["is_default = ?", true]
75 validates_associated :repository, :wiki 75 validates_associated :repository, :wiki
76 validates_length_of :name, :maximum => 255 76 validates_length_of :name, :maximum => 255
77 validates_length_of :homepage, :maximum => 255 77 validates_length_of :homepage, :maximum => 255
78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH 78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 # donwcase letters, digits, dashes but not digits only 79 # donwcase letters, digits, dashes but not digits only
80 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? } 80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 # reserved words 81 # reserved words
82 validates_exclusion_of :identifier, :in => %w( new ) 82 validates_exclusion_of :identifier, :in => %w( new )
83 83
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} 84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
85 before_destroy :delete_all_members 86 before_destroy :delete_all_members
86 87
87 scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } 88 scope :has_module, lambda {|mod|
88 scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} 89 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
89 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} } 90 }
90 scope :all_public, { :conditions => { :is_public => true } } 91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
91 scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} 92 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
93 scope :all_public, lambda { where(:is_public => true) }
92 scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } } 94 scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } }
95 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
93 scope :allowed_to, lambda {|*args| 96 scope :allowed_to, lambda {|*args|
94 user = User.current 97 user = User.current
95 permission = nil 98 permission = nil
96 if args.first.is_a?(Symbol) 99 if args.first.is_a?(Symbol)
97 permission = args.shift 100 permission = args.shift
98 else 101 else
99 user = args.shift 102 user = args.shift
100 permission = args.shift 103 permission = args.shift
101 end 104 end
102 { :conditions => Project.allowed_to_condition(user, permission, *args) } 105 where(Project.allowed_to_condition(user, permission, *args))
103 } 106 }
104 scope :like, lambda {|arg| 107 scope :like, lambda {|arg|
105 if arg.blank? 108 if arg.blank?
106 {} 109 where(nil)
107 else 110 else
108 pattern = "%#{arg.to_s.strip.downcase}%" 111 pattern = "%#{arg.to_s.strip.downcase}%"
109 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]} 112 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
110 end 113 end
111 } 114 }
112 115
113 def initialize(attributes=nil, *args) 116 def initialize(attributes=nil, *args)
114 super 117 super
122 end 125 end
123 if !initialized.key?('enabled_module_names') 126 if !initialized.key?('enabled_module_names')
124 self.enabled_module_names = Setting.default_projects_modules 127 self.enabled_module_names = Setting.default_projects_modules
125 end 128 end
126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids') 129 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
127 self.trackers = Tracker.sorted.all 130 default = Setting.default_projects_tracker_ids
131 if default.is_a?(Array)
132 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
133 else
134 self.trackers = Tracker.sorted.all
135 end
128 end 136 end
129 end 137 end
130 138
131 def identifier=(identifier) 139 def identifier=(identifier)
132 super unless identifier_frozen? 140 super unless identifier_frozen?
137 end 145 end
138 146
139 # returns latest created projects 147 # returns latest created projects
140 # non public projects will be returned only if user is a member of those 148 # non public projects will be returned only if user is a member of those
141 def self.latest(user=nil, count=5) 149 def self.latest(user=nil, count=5)
142 visible(user).find(:all, :limit => count, :order => "created_on DESC") 150 visible(user).limit(count).order("created_on DESC").all
143 end 151 end
144 152
145 # Returns true if the project is visible to +user+ or to the current user. 153 # Returns true if the project is visible to +user+ or to the current user.
146 def visible?(user=User.current) 154 def visible?(user=User.current)
147 user.allowed_to?(:view_project, self) 155 user.allowed_to?(:view_project, self)
148 end 156 end
280 288
281 def self.find_by_param(*args) 289 def self.find_by_param(*args)
282 self.find(*args) 290 self.find(*args)
283 end 291 end
284 292
293 alias :base_reload :reload
285 def reload(*args) 294 def reload(*args)
286 @shared_versions = nil 295 @shared_versions = nil
287 @rolled_up_versions = nil 296 @rolled_up_versions = nil
288 @rolled_up_trackers = nil 297 @rolled_up_trackers = nil
289 @all_issue_custom_fields = nil 298 @all_issue_custom_fields = nil
290 @all_time_entry_custom_fields = nil 299 @all_time_entry_custom_fields = nil
291 @to_param = nil 300 @to_param = nil
292 @allowed_parents = nil 301 @allowed_parents = nil
293 @allowed_permissions = nil 302 @allowed_permissions = nil
294 @actions_allowed = nil 303 @actions_allowed = nil
295 super 304 @start_date = nil
305 @due_date = nil
306 base_reload(*args)
296 end 307 end
297 308
298 def to_param 309 def to_param
299 # id is used for projects with a numeric identifier (compatibility) 310 # id is used for projects with a numeric identifier (compatibility)
300 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) 311 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
311 # Archives the project and its descendants 322 # Archives the project and its descendants
312 def archive 323 def archive
313 # Check that there is no issue of a non descendant project that is assigned 324 # Check that there is no issue of a non descendant project that is assigned
314 # to one of the project or descendant versions 325 # to one of the project or descendant versions
315 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten 326 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
316 if v_ids.any? && Issue.find(:first, :include => :project, 327 if v_ids.any? &&
317 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" + 328 Issue.
318 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids]) 329 includes(:project).
330 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
331 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
332 exists?
319 return false 333 return false
320 end 334 end
321 Project.transaction do 335 Project.transaction do
322 archive! 336 archive!
323 end 337 end
341 355
342 # Returns an array of projects the project can be moved to 356 # Returns an array of projects the project can be moved to
343 # by the current user 357 # by the current user
344 def allowed_parents 358 def allowed_parents
345 return @allowed_parents if @allowed_parents 359 return @allowed_parents if @allowed_parents
346 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects)) 360 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
347 @allowed_parents = @allowed_parents - self_and_descendants 361 @allowed_parents = @allowed_parents - self_and_descendants
348 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?) 362 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
349 @allowed_parents << nil 363 @allowed_parents << nil
350 end 364 end
351 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent) 365 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
409 end 423 end
410 424
411 # Returns an array of the trackers used by the project and its active sub projects 425 # Returns an array of the trackers used by the project and its active sub projects
412 def rolled_up_trackers 426 def rolled_up_trackers
413 @rolled_up_trackers ||= 427 @rolled_up_trackers ||=
414 Tracker.find(:all, :joins => :projects, 428 Tracker.
415 :select => "DISTINCT #{Tracker.table_name}.*", 429 joins(:projects).
416 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt], 430 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
417 :order => "#{Tracker.table_name}.position") 431 select("DISTINCT #{Tracker.table_name}.*").
432 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
433 sorted.
434 all
418 end 435 end
419 436
420 # Closes open and locked project versions that are completed 437 # Closes open and locked project versions that are completed
421 def close_completed_versions 438 def close_completed_versions
422 Version.transaction do 439 Version.transaction do
423 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version| 440 versions.where(:status => %w(open locked)).all.each do |version|
424 if version.completed? 441 if version.completed?
425 version.update_attribute(:status, 'closed') 442 version.update_attribute(:status, 'closed')
426 end 443 end
427 end 444 end
428 end 445 end
455 end 472 end
456 end 473 end
457 474
458 # Returns a hash of project users grouped by role 475 # Returns a hash of project users grouped by role
459 def users_by_role 476 def users_by_role
460 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m| 477 members.includes(:user, :roles).all.inject({}) do |h, m|
461 m.roles.each do |r| 478 m.roles.each do |r|
462 h[r] ||= [] 479 h[r] ||= []
463 h[r] << m.user 480 h[r] << m.user
464 end 481 end
465 h 482 h
548 s 565 s
549 end 566 end
550 567
551 # The earliest start date of a project, based on it's issues and versions 568 # The earliest start date of a project, based on it's issues and versions
552 def start_date 569 def start_date
553 [ 570 @start_date ||= [
554 issues.minimum('start_date'), 571 issues.minimum('start_date'),
555 shared_versions.collect(&:effective_date), 572 shared_versions.minimum('effective_date'),
556 shared_versions.collect(&:start_date) 573 Issue.fixed_version(shared_versions).minimum('start_date')
557 ].flatten.compact.min 574 ].compact.min
558 end 575 end
559 576
560 # The latest due date of an issue or version 577 # The latest due date of an issue or version
561 def due_date 578 def due_date
562 [ 579 @due_date ||= [
563 issues.maximum('due_date'), 580 issues.maximum('due_date'),
564 shared_versions.collect(&:effective_date), 581 shared_versions.maximum('effective_date'),
565 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')} 582 Issue.fixed_version(shared_versions).maximum('due_date')
566 ].flatten.compact.max 583 ].compact.max
567 end 584 end
568 585
569 def overdue? 586 def overdue?
570 active? && !due_date.nil? && (due_date < Date.today) 587 active? && !due_date.nil? && (due_date < Date.today)
571 end 588 end
577 total = self_and_descendants.collect(&:completed_percent).sum 594 total = self_and_descendants.collect(&:completed_percent).sum
578 595
579 total / self_and_descendants.count 596 total / self_and_descendants.count
580 else 597 else
581 if versions.count > 0 598 if versions.count > 0
582 total = versions.collect(&:completed_pourcent).sum 599 total = versions.collect(&:completed_percent).sum
583 600
584 total / versions.count 601 total / versions.count
585 else 602 else
586 100 603 100
587 end 604 end
660 'has_welcome_page' 677 'has_welcome_page'
661 678
662 safe_attributes 'enabled_module_names', 679 safe_attributes 'enabled_module_names',
663 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) } 680 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
664 681
682 safe_attributes 'inherit_members',
683 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
684
665 # Returns an array of projects that are in this project's hierarchy 685 # Returns an array of projects that are in this project's hierarchy
666 # 686 #
667 # Example: parents, children, siblings 687 # Example: parents, children, siblings
668 def hierarchy 688 def hierarchy
669 parents = project.self_and_ancestors || [] 689 parents = project.self_and_ancestors || []
671 project_hierarchy = parents | descendants # Set union 691 project_hierarchy = parents | descendants # Set union
672 end 692 end
673 693
674 # Returns an auto-generated project identifier based on the last identifier used 694 # Returns an auto-generated project identifier based on the last identifier used
675 def self.next_identifier 695 def self.next_identifier
676 p = Project.find(:first, :order => 'created_on DESC') 696 p = Project.order('id DESC').first
677 p.nil? ? nil : p.identifier.to_s.succ 697 p.nil? ? nil : p.identifier.to_s.succ
678 end 698 end
679 699
680 # Copies and saves the Project instance based on the +project+. 700 # Copies and saves the Project instance based on the +project+.
681 # Duplicates the source project's: 701 # Duplicates the source project's:
708 save 728 save
709 end 729 end
710 end 730 end
711 end 731 end
712 732
713 733 # Returns a new unsaved Project instance with attributes copied from +project+
714 # Copies +project+ and returns the new instance. This will not save
715 # the copy
716 def self.copy_from(project) 734 def self.copy_from(project)
717 begin 735 project = project.is_a?(Project) ? project : Project.find(project)
718 project = project.is_a?(Project) ? project : Project.find(project) 736 # clear unique attributes
719 if project 737 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
720 # clear unique attributes 738 copy = Project.new(attributes)
721 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt') 739 copy.enabled_modules = project.enabled_modules
722 copy = Project.new(attributes) 740 copy.trackers = project.trackers
723 copy.enabled_modules = project.enabled_modules 741 copy.custom_values = project.custom_values.collect {|v| v.clone}
724 copy.trackers = project.trackers 742 copy.issue_custom_fields = project.issue_custom_fields
725 copy.custom_values = project.custom_values.collect {|v| v.clone} 743 copy
726 copy.issue_custom_fields = project.issue_custom_fields
727 return copy
728 else
729 return nil
730 end
731 rescue ActiveRecord::RecordNotFound
732 return nil
733 end
734 end 744 end
735 745
736 # Yields the given block for each project with its level in the tree 746 # Yields the given block for each project with its level in the tree
737 def self.project_tree(projects, &block) 747 def self.project_tree(projects, &block)
738 ancestors = [] 748 ancestors = []
744 ancestors << project 754 ancestors << project
745 end 755 end
746 end 756 end
747 757
748 private 758 private
759
760 def after_parent_changed(parent_was)
761 remove_inherited_member_roles
762 add_inherited_member_roles
763 end
764
765 def update_inherited_members
766 if parent
767 if inherit_members? && !inherit_members_was
768 remove_inherited_member_roles
769 add_inherited_member_roles
770 elsif !inherit_members? && inherit_members_was
771 remove_inherited_member_roles
772 end
773 end
774 end
775
776 def remove_inherited_member_roles
777 member_roles = memberships.map(&:member_roles).flatten
778 member_role_ids = member_roles.map(&:id)
779 member_roles.each do |member_role|
780 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
781 member_role.destroy
782 end
783 end
784 end
785
786 def add_inherited_member_roles
787 if inherit_members? && parent
788 parent.memberships.each do |parent_member|
789 member = Member.find_or_new(self.id, parent_member.user_id)
790 parent_member.member_roles.each do |parent_member_role|
791 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
792 end
793 member.save!
794 end
795 end
796 end
749 797
750 # Copies wiki from +project+ 798 # Copies wiki from +project+
751 def copy_wiki(project) 799 def copy_wiki(project)
752 # Check that the source project has a wiki first 800 # Check that the source project has a wiki first
753 unless project.wiki.nil? 801 unless project.wiki.nil?
806 version.update_attribute :status, 'open' 854 version.update_attribute :status, 'open'
807 end 855 end
808 856
809 # Get issues sorted by root_id, lft so that parent issues 857 # Get issues sorted by root_id, lft so that parent issues
810 # get copied before their children 858 # get copied before their children
811 project.issues.find(:all, :order => 'root_id, lft').each do |issue| 859 project.issues.reorder('root_id, lft').all.each do |issue|
812 new_issue = Issue.new 860 new_issue = Issue.new
813 new_issue.copy_from(issue, :subtasks => false, :link => false) 861 new_issue.copy_from(issue, :subtasks => false, :link => false)
814 new_issue.project = self 862 new_issue.project = self
863 # Changing project resets the custom field values
864 # TODO: handle this in Issue#project=
865 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
815 # Reassign fixed_versions by name, since names are unique per project 866 # Reassign fixed_versions by name, since names are unique per project
816 if issue.fixed_version && issue.fixed_version.project == project 867 if issue.fixed_version && issue.fixed_version.project == project
817 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name} 868 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
818 end 869 end
819 # Reassign the category by name, since names are unique per project 870 # Reassign the category by name, since names are unique per project
892 end 943 end
893 944
894 # Copies queries from +project+ 945 # Copies queries from +project+
895 def copy_queries(project) 946 def copy_queries(project)
896 project.queries.each do |query| 947 project.queries.each do |query|
897 new_query = ::Query.new 948 new_query = IssueQuery.new
898 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") 949 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
899 new_query.sort_criteria = query.sort_criteria if query.sort_criteria 950 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
900 new_query.project = self 951 new_query.project = self
901 new_query.user_id = query.user_id 952 new_query.user_id = query.user_id
902 self.queries << new_query 953 self.queries << new_query
949 1000
950 # Returns the systemwide active activities merged with the project specific overrides 1001 # Returns the systemwide active activities merged with the project specific overrides
951 def system_activities_and_project_overrides(include_inactive=false) 1002 def system_activities_and_project_overrides(include_inactive=false)
952 if include_inactive 1003 if include_inactive
953 return TimeEntryActivity.shared. 1004 return TimeEntryActivity.shared.
954 find(:all, 1005 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
955 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
956 self.time_entry_activities 1006 self.time_entry_activities
957 else 1007 else
958 return TimeEntryActivity.shared.active. 1008 return TimeEntryActivity.shared.active.
959 find(:all, 1009 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
960 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
961 self.time_entry_activities.active 1010 self.time_entry_activities.active
962 end 1011 end
963 end 1012 end
964 1013
965 # Archives subprojects recursively 1014 # Archives subprojects recursively
974 set_or_update_position_under(parent) 1023 set_or_update_position_under(parent)
975 end 1024 end
976 1025
977 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted 1026 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
978 def set_or_update_position_under(target_parent) 1027 def set_or_update_position_under(target_parent)
1028 parent_was = parent
979 sibs = (target_parent.nil? ? self.class.roots : target_parent.children) 1029 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
980 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } 1030 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
981 1031
982 if to_be_inserted_before 1032 if to_be_inserted_before
983 move_to_left_of(to_be_inserted_before) 1033 move_to_left_of(to_be_inserted_before)
990 end 1040 end
991 else 1041 else
992 # move_to_child_of adds the project in last (ie.right) position 1042 # move_to_child_of adds the project in last (ie.right) position
993 move_to_child_of(target_parent) 1043 move_to_child_of(target_parent)
994 end 1044 end
1045 if parent_was != target_parent
1046 after_parent_changed(parent_was)
1047 end
995 end 1048 end
996 end 1049 end