##// END OF EJS Templates
Backported r3354 from trunk....
Jean-Philippe Lang -
r3267:69e5d91a2d2b
parent child
Show More
@@ -0,0 +1,58
1 # Redmine - project management software
2 # Copyright (C) 2006-2010 Jean-Philippe Lang
3 #
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
18 require File.dirname(__FILE__) + '/../test_helper'
19
20 class ProjectNestedSetTest < ActiveSupport::TestCase
21
22 def setup
23 Project.delete_all
24 end
25
26 def test_destroy_root_and_chldren_should_not_mess_up_the_tree
27 a = Project.create!(:name => 'Project A', :identifier => 'projecta')
28 a1 = Project.create!(:name => 'Project A1', :identifier => 'projecta1')
29 a2 = Project.create!(:name => 'Project A2', :identifier => 'projecta2')
30 a1.set_parent!(a)
31 a2.set_parent!(a)
32 b = Project.create!(:name => 'Project B', :identifier => 'projectb')
33 b1 = Project.create!(:name => 'Project B1', :identifier => 'projectb1')
34 b1.set_parent!(b)
35
36 a.reload
37 a1.reload
38 a2.reload
39 b.reload
40 b1.reload
41
42 assert_equal [nil, 1, 6], [a.parent_id, a.lft, a.rgt]
43 assert_equal [a.id, 2, 3], [a1.parent_id, a1.lft, a1.rgt]
44 assert_equal [a.id, 4, 5], [a2.parent_id, a2.lft, a2.rgt]
45 assert_equal [nil, 7, 10], [b.parent_id, b.lft, b.rgt]
46 assert_equal [b.id, 8, 9], [b1.parent_id, b1.lft, b1.rgt]
47
48 assert_difference 'Project.count', -3 do
49 a.destroy
50 end
51
52 b.reload
53 b1.reload
54
55 assert_equal [nil, 1, 4], [b.parent_id, b.lft, b.rgt]
56 assert_equal [b.id, 2, 3], [b1.parent_id, b1.lft, b1.rgt]
57 end
58 end No newline at end of file
@@ -1,677 +1,684
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Specific overidden Activities
23 # Specific overidden Activities
24 has_many :time_entry_activities
24 has_many :time_entry_activities
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
25 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
26 has_many :memberships, :class_name => 'Member'
26 has_many :memberships, :class_name => 'Member'
27 has_many :member_principals, :class_name => 'Member',
27 has_many :member_principals, :class_name => 'Member',
28 :include => :principal,
28 :include => :principal,
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
29 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
30 has_many :users, :through => :members
30 has_many :users, :through => :members
31 has_many :principals, :through => :member_principals, :source => :principal
31 has_many :principals, :through => :member_principals, :source => :principal
32
32
33 has_many :enabled_modules, :dependent => :delete_all
33 has_many :enabled_modules, :dependent => :delete_all
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
34 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
35 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
36 has_many :issue_changes, :through => :issues, :source => :journals
36 has_many :issue_changes, :through => :issues, :source => :journals
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
37 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
38 has_many :time_entries, :dependent => :delete_all
38 has_many :time_entries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
39 has_many :queries, :dependent => :delete_all
40 has_many :documents, :dependent => :destroy
40 has_many :documents, :dependent => :destroy
41 has_many :news, :dependent => :delete_all, :include => :author
41 has_many :news, :dependent => :delete_all, :include => :author
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
42 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
43 has_many :boards, :dependent => :destroy, :order => "position ASC"
44 has_one :repository, :dependent => :destroy
44 has_one :repository, :dependent => :destroy
45 has_many :changesets, :through => :repository
45 has_many :changesets, :through => :repository
46 has_one :wiki, :dependent => :destroy
46 has_one :wiki, :dependent => :destroy
47 # Custom field for the project issues
47 # Custom field for the project issues
48 has_and_belongs_to_many :issue_custom_fields,
48 has_and_belongs_to_many :issue_custom_fields,
49 :class_name => 'IssueCustomField',
49 :class_name => 'IssueCustomField',
50 :order => "#{CustomField.table_name}.position",
50 :order => "#{CustomField.table_name}.position",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
51 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
52 :association_foreign_key => 'custom_field_id'
52 :association_foreign_key => 'custom_field_id'
53
53
54 acts_as_nested_set :order => 'name', :dependent => :destroy
54 acts_as_nested_set :order => 'name'
55 acts_as_attachable :view_permission => :view_files,
55 acts_as_attachable :view_permission => :view_files,
56 :delete_permission => :manage_files
56 :delete_permission => :manage_files
57
57
58 acts_as_customizable
58 acts_as_customizable
59 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
59 acts_as_searchable :columns => ['name', 'description'], :project_key => 'id', :permission => nil
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
60 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
61 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o.id}},
62 :author => nil
62 :author => nil
63
63
64 attr_protected :status, :enabled_module_names
64 attr_protected :status, :enabled_module_names
65
65
66 validates_presence_of :name, :identifier
66 validates_presence_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
67 validates_uniqueness_of :name, :identifier
68 validates_associated :repository, :wiki
68 validates_associated :repository, :wiki
69 validates_length_of :name, :maximum => 30
69 validates_length_of :name, :maximum => 30
70 validates_length_of :homepage, :maximum => 255
70 validates_length_of :homepage, :maximum => 255
71 validates_length_of :identifier, :in => 1..20
71 validates_length_of :identifier, :in => 1..20
72 # donwcase letters, digits, dashes but not digits only
72 # donwcase letters, digits, dashes but not digits only
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
73 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
74 # reserved words
74 # reserved words
75 validates_exclusion_of :identifier, :in => %w( new )
75 validates_exclusion_of :identifier, :in => %w( new )
76
76
77 before_destroy :delete_all_members
77 before_destroy :delete_all_members, :destroy_children
78
78
79 named_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] } }
79 named_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] } }
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
80 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
81 named_scope :all_public, { :conditions => { :is_public => true } }
81 named_scope :all_public, { :conditions => { :is_public => true } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
82 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
83
83
84 def identifier=(identifier)
84 def identifier=(identifier)
85 super unless identifier_frozen?
85 super unless identifier_frozen?
86 end
86 end
87
87
88 def identifier_frozen?
88 def identifier_frozen?
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
89 errors[:identifier].nil? && !(new_record? || identifier.blank?)
90 end
90 end
91
91
92 # returns latest created projects
92 # returns latest created projects
93 # non public projects will be returned only if user is a member of those
93 # non public projects will be returned only if user is a member of those
94 def self.latest(user=nil, count=5)
94 def self.latest(user=nil, count=5)
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
95 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
96 end
96 end
97
97
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
98 # Returns a SQL :conditions string used to find all active projects for the specified user.
99 #
99 #
100 # Examples:
100 # Examples:
101 # Projects.visible_by(admin) => "projects.status = 1"
101 # Projects.visible_by(admin) => "projects.status = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
102 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
103 def self.visible_by(user=nil)
103 def self.visible_by(user=nil)
104 user ||= User.current
104 user ||= User.current
105 if user && user.admin?
105 if user && user.admin?
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
106 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
107 elsif user && user.memberships.any?
107 elsif user && user.memberships.any?
108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
108 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
109 else
109 else
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
110 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
111 end
111 end
112 end
112 end
113
113
114 def self.allowed_to_condition(user, permission, options={})
114 def self.allowed_to_condition(user, permission, options={})
115 statements = []
115 statements = []
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
116 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
117 if perm = Redmine::AccessControl.permission(permission)
117 if perm = Redmine::AccessControl.permission(permission)
118 unless perm.project_module.nil?
118 unless perm.project_module.nil?
119 # If the permission belongs to a project module, make sure the module is enabled
119 # If the permission belongs to a project module, make sure the module is enabled
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
120 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
121 end
121 end
122 end
122 end
123 if options[:project]
123 if options[:project]
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
124 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
125 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
126 base_statement = "(#{project_statement}) AND (#{base_statement})"
127 end
127 end
128 if user.admin?
128 if user.admin?
129 # no restriction
129 # no restriction
130 else
130 else
131 statements << "1=0"
131 statements << "1=0"
132 if user.logged?
132 if user.logged?
133 if Role.non_member.allowed_to?(permission) && !options[:member]
133 if Role.non_member.allowed_to?(permission) && !options[:member]
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
134 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
135 end
135 end
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
136 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
137 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
138 else
138 else
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
139 if Role.anonymous.allowed_to?(permission) && !options[:member]
140 # anonymous user allowed on public project
140 # anonymous user allowed on public project
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
141 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
142 end
142 end
143 end
143 end
144 end
144 end
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
145 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
146 end
146 end
147
147
148 # Returns the Systemwide and project specific activities
148 # Returns the Systemwide and project specific activities
149 def activities(include_inactive=false)
149 def activities(include_inactive=false)
150 if include_inactive
150 if include_inactive
151 return all_activities
151 return all_activities
152 else
152 else
153 return active_activities
153 return active_activities
154 end
154 end
155 end
155 end
156
156
157 # Will create a new Project specific Activity or update an existing one
157 # Will create a new Project specific Activity or update an existing one
158 #
158 #
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
159 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
160 # does not successfully save.
160 # does not successfully save.
161 def update_or_create_time_entry_activity(id, activity_hash)
161 def update_or_create_time_entry_activity(id, activity_hash)
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
162 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
163 self.create_time_entry_activity_if_needed(activity_hash)
163 self.create_time_entry_activity_if_needed(activity_hash)
164 else
164 else
165 activity = project.time_entry_activities.find_by_id(id.to_i)
165 activity = project.time_entry_activities.find_by_id(id.to_i)
166 activity.update_attributes(activity_hash) if activity
166 activity.update_attributes(activity_hash) if activity
167 end
167 end
168 end
168 end
169
169
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
170 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
171 #
171 #
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
172 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
173 # does not successfully save.
173 # does not successfully save.
174 def create_time_entry_activity_if_needed(activity)
174 def create_time_entry_activity_if_needed(activity)
175 if activity['parent_id']
175 if activity['parent_id']
176
176
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
177 parent_activity = TimeEntryActivity.find(activity['parent_id'])
178 activity['name'] = parent_activity.name
178 activity['name'] = parent_activity.name
179 activity['position'] = parent_activity.position
179 activity['position'] = parent_activity.position
180
180
181 if Enumeration.overridding_change?(activity, parent_activity)
181 if Enumeration.overridding_change?(activity, parent_activity)
182 project_activity = self.time_entry_activities.create(activity)
182 project_activity = self.time_entry_activities.create(activity)
183
183
184 if project_activity.new_record?
184 if project_activity.new_record?
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
185 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
186 else
186 else
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
187 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
188 end
188 end
189 end
189 end
190 end
190 end
191 end
191 end
192
192
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
193 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
194 #
194 #
195 # Examples:
195 # Examples:
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
196 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
197 # project.project_condition(false) => "projects.id = 1"
197 # project.project_condition(false) => "projects.id = 1"
198 def project_condition(with_subprojects)
198 def project_condition(with_subprojects)
199 cond = "#{Project.table_name}.id = #{id}"
199 cond = "#{Project.table_name}.id = #{id}"
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
200 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
201 cond
201 cond
202 end
202 end
203
203
204 def self.find(*args)
204 def self.find(*args)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
205 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
206 project = find_by_identifier(*args)
206 project = find_by_identifier(*args)
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
207 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
208 project
208 project
209 else
209 else
210 super
210 super
211 end
211 end
212 end
212 end
213
213
214 def to_param
214 def to_param
215 # id is used for projects with a numeric identifier (compatibility)
215 # id is used for projects with a numeric identifier (compatibility)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
216 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
217 end
217 end
218
218
219 def active?
219 def active?
220 self.status == STATUS_ACTIVE
220 self.status == STATUS_ACTIVE
221 end
221 end
222
222
223 # Archives the project and its descendants
223 # Archives the project and its descendants
224 def archive
224 def archive
225 # Check that there is no issue of a non descendant project that is assigned
225 # Check that there is no issue of a non descendant project that is assigned
226 # to one of the project or descendant versions
226 # to one of the project or descendant versions
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
227 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
228 if v_ids.any? && Issue.find(:first, :include => :project,
228 if v_ids.any? && Issue.find(:first, :include => :project,
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
229 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
230 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
231 return false
231 return false
232 end
232 end
233 Project.transaction do
233 Project.transaction do
234 archive!
234 archive!
235 end
235 end
236 true
236 true
237 end
237 end
238
238
239 # Unarchives the project
239 # Unarchives the project
240 # All its ancestors must be active
240 # All its ancestors must be active
241 def unarchive
241 def unarchive
242 return false if ancestors.detect {|a| !a.active?}
242 return false if ancestors.detect {|a| !a.active?}
243 update_attribute :status, STATUS_ACTIVE
243 update_attribute :status, STATUS_ACTIVE
244 end
244 end
245
245
246 # Returns an array of projects the project can be moved to
246 # Returns an array of projects the project can be moved to
247 # by the current user
247 # by the current user
248 def allowed_parents
248 def allowed_parents
249 return @allowed_parents if @allowed_parents
249 return @allowed_parents if @allowed_parents
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
250 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
251 @allowed_parents = @allowed_parents - self_and_descendants
251 @allowed_parents = @allowed_parents - self_and_descendants
252 if User.current.allowed_to?(:add_project, nil, :global => true)
252 if User.current.allowed_to?(:add_project, nil, :global => true)
253 @allowed_parents << nil
253 @allowed_parents << nil
254 end
254 end
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
255 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
256 @allowed_parents << parent
256 @allowed_parents << parent
257 end
257 end
258 @allowed_parents
258 @allowed_parents
259 end
259 end
260
260
261 # Sets the parent of the project with authorization check
261 # Sets the parent of the project with authorization check
262 def set_allowed_parent!(p)
262 def set_allowed_parent!(p)
263 unless p.nil? || p.is_a?(Project)
263 unless p.nil? || p.is_a?(Project)
264 if p.to_s.blank?
264 if p.to_s.blank?
265 p = nil
265 p = nil
266 else
266 else
267 p = Project.find_by_id(p)
267 p = Project.find_by_id(p)
268 return false unless p
268 return false unless p
269 end
269 end
270 end
270 end
271 if p.nil?
271 if p.nil?
272 if !new_record? && allowed_parents.empty?
272 if !new_record? && allowed_parents.empty?
273 return false
273 return false
274 end
274 end
275 elsif !allowed_parents.include?(p)
275 elsif !allowed_parents.include?(p)
276 return false
276 return false
277 end
277 end
278 set_parent!(p)
278 set_parent!(p)
279 end
279 end
280
280
281 # Sets the parent of the project
281 # Sets the parent of the project
282 # Argument can be either a Project, a String, a Fixnum or nil
282 # Argument can be either a Project, a String, a Fixnum or nil
283 def set_parent!(p)
283 def set_parent!(p)
284 unless p.nil? || p.is_a?(Project)
284 unless p.nil? || p.is_a?(Project)
285 if p.to_s.blank?
285 if p.to_s.blank?
286 p = nil
286 p = nil
287 else
287 else
288 p = Project.find_by_id(p)
288 p = Project.find_by_id(p)
289 return false unless p
289 return false unless p
290 end
290 end
291 end
291 end
292 if p == parent && !p.nil?
292 if p == parent && !p.nil?
293 # Nothing to do
293 # Nothing to do
294 true
294 true
295 elsif p.nil? || (p.active? && move_possible?(p))
295 elsif p.nil? || (p.active? && move_possible?(p))
296 # Insert the project so that target's children or root projects stay alphabetically sorted
296 # Insert the project so that target's children or root projects stay alphabetically sorted
297 sibs = (p.nil? ? self.class.roots : p.children)
297 sibs = (p.nil? ? self.class.roots : p.children)
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
298 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
299 if to_be_inserted_before
299 if to_be_inserted_before
300 move_to_left_of(to_be_inserted_before)
300 move_to_left_of(to_be_inserted_before)
301 elsif p.nil?
301 elsif p.nil?
302 if sibs.empty?
302 if sibs.empty?
303 # move_to_root adds the project in first (ie. left) position
303 # move_to_root adds the project in first (ie. left) position
304 move_to_root
304 move_to_root
305 else
305 else
306 move_to_right_of(sibs.last) unless self == sibs.last
306 move_to_right_of(sibs.last) unless self == sibs.last
307 end
307 end
308 else
308 else
309 # move_to_child_of adds the project in last (ie.right) position
309 # move_to_child_of adds the project in last (ie.right) position
310 move_to_child_of(p)
310 move_to_child_of(p)
311 end
311 end
312 Issue.update_versions_from_hierarchy_change(self)
312 Issue.update_versions_from_hierarchy_change(self)
313 true
313 true
314 else
314 else
315 # Can not move to the given target
315 # Can not move to the given target
316 false
316 false
317 end
317 end
318 end
318 end
319
319
320 # Returns an array of the trackers used by the project and its active sub projects
320 # Returns an array of the trackers used by the project and its active sub projects
321 def rolled_up_trackers
321 def rolled_up_trackers
322 @rolled_up_trackers ||=
322 @rolled_up_trackers ||=
323 Tracker.find(:all, :include => :projects,
323 Tracker.find(:all, :include => :projects,
324 :select => "DISTINCT #{Tracker.table_name}.*",
324 :select => "DISTINCT #{Tracker.table_name}.*",
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
325 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
326 :order => "#{Tracker.table_name}.position")
326 :order => "#{Tracker.table_name}.position")
327 end
327 end
328
328
329 # Closes open and locked project versions that are completed
329 # Closes open and locked project versions that are completed
330 def close_completed_versions
330 def close_completed_versions
331 Version.transaction do
331 Version.transaction do
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
332 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
333 if version.completed?
333 if version.completed?
334 version.update_attribute(:status, 'closed')
334 version.update_attribute(:status, 'closed')
335 end
335 end
336 end
336 end
337 end
337 end
338 end
338 end
339
339
340 # Returns a scope of the Versions used by the project
340 # Returns a scope of the Versions used by the project
341 def shared_versions
341 def shared_versions
342 @shared_versions ||=
342 @shared_versions ||=
343 Version.scoped(:include => :project,
343 Version.scoped(:include => :project,
344 :conditions => "#{Project.table_name}.id = #{id}" +
344 :conditions => "#{Project.table_name}.id = #{id}" +
345 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
345 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
346 " #{Version.table_name}.sharing = 'system'" +
346 " #{Version.table_name}.sharing = 'system'" +
347 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
347 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
348 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
348 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
349 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
349 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
350 "))")
350 "))")
351 end
351 end
352
352
353 # Returns a hash of project users grouped by role
353 # Returns a hash of project users grouped by role
354 def users_by_role
354 def users_by_role
355 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
355 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
356 m.roles.each do |r|
356 m.roles.each do |r|
357 h[r] ||= []
357 h[r] ||= []
358 h[r] << m.user
358 h[r] << m.user
359 end
359 end
360 h
360 h
361 end
361 end
362 end
362 end
363
363
364 # Deletes all project's members
364 # Deletes all project's members
365 def delete_all_members
365 def delete_all_members
366 me, mr = Member.table_name, MemberRole.table_name
366 me, mr = Member.table_name, MemberRole.table_name
367 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
367 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
368 Member.delete_all(['project_id = ?', id])
368 Member.delete_all(['project_id = ?', id])
369 end
369 end
370
370
371 # Users issues can be assigned to
371 # Users issues can be assigned to
372 def assignable_users
372 def assignable_users
373 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
373 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
374 end
374 end
375
375
376 # Returns the mail adresses of users that should be always notified on project events
376 # Returns the mail adresses of users that should be always notified on project events
377 def recipients
377 def recipients
378 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
378 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user.mail}
379 end
379 end
380
380
381 # Returns the users that should be notified on project events
381 # Returns the users that should be notified on project events
382 def notified_users
382 def notified_users
383 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
383 members.select {|m| m.mail_notification? || m.user.mail_notification?}.collect {|m| m.user}
384 end
384 end
385
385
386 # Returns an array of all custom fields enabled for project issues
386 # Returns an array of all custom fields enabled for project issues
387 # (explictly associated custom fields and custom fields enabled for all projects)
387 # (explictly associated custom fields and custom fields enabled for all projects)
388 def all_issue_custom_fields
388 def all_issue_custom_fields
389 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
389 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
390 end
390 end
391
391
392 def project
392 def project
393 self
393 self
394 end
394 end
395
395
396 def <=>(project)
396 def <=>(project)
397 name.downcase <=> project.name.downcase
397 name.downcase <=> project.name.downcase
398 end
398 end
399
399
400 def to_s
400 def to_s
401 name
401 name
402 end
402 end
403
403
404 # Returns a short description of the projects (first lines)
404 # Returns a short description of the projects (first lines)
405 def short_description(length = 255)
405 def short_description(length = 255)
406 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
406 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
407 end
407 end
408
408
409 # Return true if this project is allowed to do the specified action.
409 # Return true if this project is allowed to do the specified action.
410 # action can be:
410 # action can be:
411 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
411 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
412 # * a permission Symbol (eg. :edit_project)
412 # * a permission Symbol (eg. :edit_project)
413 def allows_to?(action)
413 def allows_to?(action)
414 if action.is_a? Hash
414 if action.is_a? Hash
415 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
415 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
416 else
416 else
417 allowed_permissions.include? action
417 allowed_permissions.include? action
418 end
418 end
419 end
419 end
420
420
421 def module_enabled?(module_name)
421 def module_enabled?(module_name)
422 module_name = module_name.to_s
422 module_name = module_name.to_s
423 enabled_modules.detect {|m| m.name == module_name}
423 enabled_modules.detect {|m| m.name == module_name}
424 end
424 end
425
425
426 def enabled_module_names=(module_names)
426 def enabled_module_names=(module_names)
427 if module_names && module_names.is_a?(Array)
427 if module_names && module_names.is_a?(Array)
428 module_names = module_names.collect(&:to_s)
428 module_names = module_names.collect(&:to_s)
429 # remove disabled modules
429 # remove disabled modules
430 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
430 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
431 # add new modules
431 # add new modules
432 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
432 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
433 else
433 else
434 enabled_modules.clear
434 enabled_modules.clear
435 end
435 end
436 end
436 end
437
437
438 # Returns an auto-generated project identifier based on the last identifier used
438 # Returns an auto-generated project identifier based on the last identifier used
439 def self.next_identifier
439 def self.next_identifier
440 p = Project.find(:first, :order => 'created_on DESC')
440 p = Project.find(:first, :order => 'created_on DESC')
441 p.nil? ? nil : p.identifier.to_s.succ
441 p.nil? ? nil : p.identifier.to_s.succ
442 end
442 end
443
443
444 # Copies and saves the Project instance based on the +project+.
444 # Copies and saves the Project instance based on the +project+.
445 # Duplicates the source project's:
445 # Duplicates the source project's:
446 # * Wiki
446 # * Wiki
447 # * Versions
447 # * Versions
448 # * Categories
448 # * Categories
449 # * Issues
449 # * Issues
450 # * Members
450 # * Members
451 # * Queries
451 # * Queries
452 #
452 #
453 # Accepts an +options+ argument to specify what to copy
453 # Accepts an +options+ argument to specify what to copy
454 #
454 #
455 # Examples:
455 # Examples:
456 # project.copy(1) # => copies everything
456 # project.copy(1) # => copies everything
457 # project.copy(1, :only => 'members') # => copies members only
457 # project.copy(1, :only => 'members') # => copies members only
458 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
458 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
459 def copy(project, options={})
459 def copy(project, options={})
460 project = project.is_a?(Project) ? project : Project.find(project)
460 project = project.is_a?(Project) ? project : Project.find(project)
461
461
462 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
462 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
463 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
463 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
464
464
465 Project.transaction do
465 Project.transaction do
466 if save
466 if save
467 reload
467 reload
468 to_be_copied.each do |name|
468 to_be_copied.each do |name|
469 send "copy_#{name}", project
469 send "copy_#{name}", project
470 end
470 end
471 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
471 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
472 save
472 save
473 end
473 end
474 end
474 end
475 end
475 end
476
476
477
477
478 # Copies +project+ and returns the new instance. This will not save
478 # Copies +project+ and returns the new instance. This will not save
479 # the copy
479 # the copy
480 def self.copy_from(project)
480 def self.copy_from(project)
481 begin
481 begin
482 project = project.is_a?(Project) ? project : Project.find(project)
482 project = project.is_a?(Project) ? project : Project.find(project)
483 if project
483 if project
484 # clear unique attributes
484 # clear unique attributes
485 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
485 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
486 copy = Project.new(attributes)
486 copy = Project.new(attributes)
487 copy.enabled_modules = project.enabled_modules
487 copy.enabled_modules = project.enabled_modules
488 copy.trackers = project.trackers
488 copy.trackers = project.trackers
489 copy.custom_values = project.custom_values.collect {|v| v.clone}
489 copy.custom_values = project.custom_values.collect {|v| v.clone}
490 copy.issue_custom_fields = project.issue_custom_fields
490 copy.issue_custom_fields = project.issue_custom_fields
491 return copy
491 return copy
492 else
492 else
493 return nil
493 return nil
494 end
494 end
495 rescue ActiveRecord::RecordNotFound
495 rescue ActiveRecord::RecordNotFound
496 return nil
496 return nil
497 end
497 end
498 end
498 end
499
499
500 private
500 private
501
501
502 # Destroys children before destroying self
503 def destroy_children
504 children.each do |child|
505 child.destroy
506 end
507 end
508
502 # Copies wiki from +project+
509 # Copies wiki from +project+
503 def copy_wiki(project)
510 def copy_wiki(project)
504 # Check that the source project has a wiki first
511 # Check that the source project has a wiki first
505 unless project.wiki.nil?
512 unless project.wiki.nil?
506 self.wiki ||= Wiki.new
513 self.wiki ||= Wiki.new
507 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
514 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
508 project.wiki.pages.each do |page|
515 project.wiki.pages.each do |page|
509 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
516 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
510 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
517 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
511 new_wiki_page.content = new_wiki_content
518 new_wiki_page.content = new_wiki_content
512 wiki.pages << new_wiki_page
519 wiki.pages << new_wiki_page
513 end
520 end
514 end
521 end
515 end
522 end
516
523
517 # Copies versions from +project+
524 # Copies versions from +project+
518 def copy_versions(project)
525 def copy_versions(project)
519 project.versions.each do |version|
526 project.versions.each do |version|
520 new_version = Version.new
527 new_version = Version.new
521 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
528 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
522 self.versions << new_version
529 self.versions << new_version
523 end
530 end
524 end
531 end
525
532
526 # Copies issue categories from +project+
533 # Copies issue categories from +project+
527 def copy_issue_categories(project)
534 def copy_issue_categories(project)
528 project.issue_categories.each do |issue_category|
535 project.issue_categories.each do |issue_category|
529 new_issue_category = IssueCategory.new
536 new_issue_category = IssueCategory.new
530 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
537 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
531 self.issue_categories << new_issue_category
538 self.issue_categories << new_issue_category
532 end
539 end
533 end
540 end
534
541
535 # Copies issues from +project+
542 # Copies issues from +project+
536 def copy_issues(project)
543 def copy_issues(project)
537 # Stores the source issue id as a key and the copied issues as the
544 # Stores the source issue id as a key and the copied issues as the
538 # value. Used to map the two togeather for issue relations.
545 # value. Used to map the two togeather for issue relations.
539 issues_map = {}
546 issues_map = {}
540
547
541 project.issues.each do |issue|
548 project.issues.each do |issue|
542 new_issue = Issue.new
549 new_issue = Issue.new
543 new_issue.copy_from(issue)
550 new_issue.copy_from(issue)
544 # Reassign fixed_versions by name, since names are unique per
551 # Reassign fixed_versions by name, since names are unique per
545 # project and the versions for self are not yet saved
552 # project and the versions for self are not yet saved
546 if issue.fixed_version
553 if issue.fixed_version
547 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
554 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
548 end
555 end
549 # Reassign the category by name, since names are unique per
556 # Reassign the category by name, since names are unique per
550 # project and the categories for self are not yet saved
557 # project and the categories for self are not yet saved
551 if issue.category
558 if issue.category
552 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
559 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
553 end
560 end
554 self.issues << new_issue
561 self.issues << new_issue
555 issues_map[issue.id] = new_issue
562 issues_map[issue.id] = new_issue
556 end
563 end
557
564
558 # Relations after in case issues related each other
565 # Relations after in case issues related each other
559 project.issues.each do |issue|
566 project.issues.each do |issue|
560 new_issue = issues_map[issue.id]
567 new_issue = issues_map[issue.id]
561
568
562 # Relations
569 # Relations
563 issue.relations_from.each do |source_relation|
570 issue.relations_from.each do |source_relation|
564 new_issue_relation = IssueRelation.new
571 new_issue_relation = IssueRelation.new
565 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
572 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
566 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
573 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
567 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
574 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
568 new_issue_relation.issue_to = source_relation.issue_to
575 new_issue_relation.issue_to = source_relation.issue_to
569 end
576 end
570 new_issue.relations_from << new_issue_relation
577 new_issue.relations_from << new_issue_relation
571 end
578 end
572
579
573 issue.relations_to.each do |source_relation|
580 issue.relations_to.each do |source_relation|
574 new_issue_relation = IssueRelation.new
581 new_issue_relation = IssueRelation.new
575 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
582 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
576 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
583 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
577 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
584 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
578 new_issue_relation.issue_from = source_relation.issue_from
585 new_issue_relation.issue_from = source_relation.issue_from
579 end
586 end
580 new_issue.relations_to << new_issue_relation
587 new_issue.relations_to << new_issue_relation
581 end
588 end
582 end
589 end
583 end
590 end
584
591
585 # Copies members from +project+
592 # Copies members from +project+
586 def copy_members(project)
593 def copy_members(project)
587 project.memberships.each do |member|
594 project.memberships.each do |member|
588 new_member = Member.new
595 new_member = Member.new
589 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
596 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
590 # only copy non inherited roles
597 # only copy non inherited roles
591 # inherited roles will be added when copying the group membership
598 # inherited roles will be added when copying the group membership
592 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
599 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
593 next if role_ids.empty?
600 next if role_ids.empty?
594 new_member.role_ids = role_ids
601 new_member.role_ids = role_ids
595 new_member.project = self
602 new_member.project = self
596 self.members << new_member
603 self.members << new_member
597 end
604 end
598 end
605 end
599
606
600 # Copies queries from +project+
607 # Copies queries from +project+
601 def copy_queries(project)
608 def copy_queries(project)
602 project.queries.each do |query|
609 project.queries.each do |query|
603 new_query = Query.new
610 new_query = Query.new
604 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
611 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
605 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
612 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
606 new_query.project = self
613 new_query.project = self
607 self.queries << new_query
614 self.queries << new_query
608 end
615 end
609 end
616 end
610
617
611 # Copies boards from +project+
618 # Copies boards from +project+
612 def copy_boards(project)
619 def copy_boards(project)
613 project.boards.each do |board|
620 project.boards.each do |board|
614 new_board = Board.new
621 new_board = Board.new
615 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
622 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
616 new_board.project = self
623 new_board.project = self
617 self.boards << new_board
624 self.boards << new_board
618 end
625 end
619 end
626 end
620
627
621 def allowed_permissions
628 def allowed_permissions
622 @allowed_permissions ||= begin
629 @allowed_permissions ||= begin
623 module_names = enabled_modules.collect {|m| m.name}
630 module_names = enabled_modules.collect {|m| m.name}
624 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
631 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
625 end
632 end
626 end
633 end
627
634
628 def allowed_actions
635 def allowed_actions
629 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
636 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
630 end
637 end
631
638
632 # Returns all the active Systemwide and project specific activities
639 # Returns all the active Systemwide and project specific activities
633 def active_activities
640 def active_activities
634 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
641 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
635
642
636 if overridden_activity_ids.empty?
643 if overridden_activity_ids.empty?
637 return TimeEntryActivity.shared.active
644 return TimeEntryActivity.shared.active
638 else
645 else
639 return system_activities_and_project_overrides
646 return system_activities_and_project_overrides
640 end
647 end
641 end
648 end
642
649
643 # Returns all the Systemwide and project specific activities
650 # Returns all the Systemwide and project specific activities
644 # (inactive and active)
651 # (inactive and active)
645 def all_activities
652 def all_activities
646 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
653 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
647
654
648 if overridden_activity_ids.empty?
655 if overridden_activity_ids.empty?
649 return TimeEntryActivity.shared
656 return TimeEntryActivity.shared
650 else
657 else
651 return system_activities_and_project_overrides(true)
658 return system_activities_and_project_overrides(true)
652 end
659 end
653 end
660 end
654
661
655 # Returns the systemwide active activities merged with the project specific overrides
662 # Returns the systemwide active activities merged with the project specific overrides
656 def system_activities_and_project_overrides(include_inactive=false)
663 def system_activities_and_project_overrides(include_inactive=false)
657 if include_inactive
664 if include_inactive
658 return TimeEntryActivity.shared.
665 return TimeEntryActivity.shared.
659 find(:all,
666 find(:all,
660 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
667 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
661 self.time_entry_activities
668 self.time_entry_activities
662 else
669 else
663 return TimeEntryActivity.shared.active.
670 return TimeEntryActivity.shared.active.
664 find(:all,
671 find(:all,
665 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
672 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
666 self.time_entry_activities.active
673 self.time_entry_activities.active
667 end
674 end
668 end
675 end
669
676
670 # Archives subprojects recursively
677 # Archives subprojects recursively
671 def archive!
678 def archive!
672 children.each do |subproject|
679 children.each do |subproject|
673 subproject.send :archive!
680 subproject.send :archive!
674 end
681 end
675 update_attribute :status, STATUS_ARCHIVED
682 update_attribute :status, STATUS_ARCHIVED
676 end
683 end
677 end
684 end
@@ -1,131 +1,131
1 ---
1 ---
2 custom_fields_001:
2 custom_fields_001:
3 name: Database
3 name: Database
4 min_length: 0
4 min_length: 0
5 regexp: ""
5 regexp: ""
6 is_for_all: true
6 is_for_all: true
7 is_filter: true
7 is_filter: true
8 type: IssueCustomField
8 type: IssueCustomField
9 max_length: 0
9 max_length: 0
10 possible_values:
10 possible_values:
11 - MySQL
11 - MySQL
12 - PostgreSQL
12 - PostgreSQL
13 - Oracle
13 - Oracle
14 id: 1
14 id: 1
15 is_required: false
15 is_required: false
16 field_format: list
16 field_format: list
17 default_value: ""
17 default_value: ""
18 editable: true
18 editable: true
19 custom_fields_002:
19 custom_fields_002:
20 name: Searchable field
20 name: Searchable field
21 min_length: 1
21 min_length: 1
22 regexp: ""
22 regexp: ""
23 is_for_all: true
23 is_for_all: true
24 type: IssueCustomField
24 type: IssueCustomField
25 max_length: 100
25 max_length: 100
26 possible_values: ""
26 possible_values: ""
27 id: 2
27 id: 2
28 is_required: false
28 is_required: false
29 field_format: string
29 field_format: string
30 searchable: true
30 searchable: true
31 default_value: "Default string"
31 default_value: "Default string"
32 editable: true
32 editable: true
33 custom_fields_003:
33 custom_fields_003:
34 name: Development status
34 name: Development status
35 min_length: 0
35 min_length: 0
36 regexp: ""
36 regexp: ""
37 is_for_all: false
37 is_for_all: false
38 is_filter: true
38 is_filter: true
39 type: ProjectCustomField
39 type: ProjectCustomField
40 max_length: 0
40 max_length: 0
41 possible_values:
41 possible_values:
42 - Stable
42 - Stable
43 - Beta
43 - Beta
44 - Alpha
44 - Alpha
45 - Planning
45 - Planning
46 id: 3
46 id: 3
47 is_required: true
47 is_required: false
48 field_format: list
48 field_format: list
49 default_value: ""
49 default_value: ""
50 editable: true
50 editable: true
51 custom_fields_004:
51 custom_fields_004:
52 name: Phone number
52 name: Phone number
53 min_length: 0
53 min_length: 0
54 regexp: ""
54 regexp: ""
55 is_for_all: false
55 is_for_all: false
56 type: UserCustomField
56 type: UserCustomField
57 max_length: 0
57 max_length: 0
58 possible_values: ""
58 possible_values: ""
59 id: 4
59 id: 4
60 is_required: false
60 is_required: false
61 field_format: string
61 field_format: string
62 default_value: ""
62 default_value: ""
63 editable: true
63 editable: true
64 custom_fields_005:
64 custom_fields_005:
65 name: Money
65 name: Money
66 min_length: 0
66 min_length: 0
67 regexp: ""
67 regexp: ""
68 is_for_all: false
68 is_for_all: false
69 type: UserCustomField
69 type: UserCustomField
70 max_length: 0
70 max_length: 0
71 possible_values: ""
71 possible_values: ""
72 id: 5
72 id: 5
73 is_required: false
73 is_required: false
74 field_format: float
74 field_format: float
75 default_value: ""
75 default_value: ""
76 editable: true
76 editable: true
77 custom_fields_006:
77 custom_fields_006:
78 name: Float field
78 name: Float field
79 min_length: 0
79 min_length: 0
80 regexp: ""
80 regexp: ""
81 is_for_all: true
81 is_for_all: true
82 type: IssueCustomField
82 type: IssueCustomField
83 max_length: 0
83 max_length: 0
84 possible_values: ""
84 possible_values: ""
85 id: 6
85 id: 6
86 is_required: false
86 is_required: false
87 field_format: float
87 field_format: float
88 default_value: ""
88 default_value: ""
89 editable: true
89 editable: true
90 custom_fields_007:
90 custom_fields_007:
91 name: Billable
91 name: Billable
92 min_length: 0
92 min_length: 0
93 regexp: ""
93 regexp: ""
94 is_for_all: false
94 is_for_all: false
95 is_filter: true
95 is_filter: true
96 type: TimeEntryActivityCustomField
96 type: TimeEntryActivityCustomField
97 max_length: 0
97 max_length: 0
98 possible_values: ""
98 possible_values: ""
99 id: 7
99 id: 7
100 is_required: false
100 is_required: false
101 field_format: bool
101 field_format: bool
102 default_value: ""
102 default_value: ""
103 editable: true
103 editable: true
104 custom_fields_008:
104 custom_fields_008:
105 name: Custom date
105 name: Custom date
106 min_length: 0
106 min_length: 0
107 regexp: ""
107 regexp: ""
108 is_for_all: true
108 is_for_all: true
109 is_filter: false
109 is_filter: false
110 type: IssueCustomField
110 type: IssueCustomField
111 max_length: 0
111 max_length: 0
112 possible_values: ""
112 possible_values: ""
113 id: 8
113 id: 8
114 is_required: false
114 is_required: false
115 field_format: date
115 field_format: date
116 default_value: ""
116 default_value: ""
117 editable: true
117 editable: true
118 custom_fields_009:
118 custom_fields_009:
119 name: Project 1 cf
119 name: Project 1 cf
120 min_length: 0
120 min_length: 0
121 regexp: ""
121 regexp: ""
122 is_for_all: false
122 is_for_all: false
123 is_filter: true
123 is_filter: true
124 type: IssueCustomField
124 type: IssueCustomField
125 max_length: 0
125 max_length: 0
126 possible_values: ""
126 possible_values: ""
127 id: 9
127 id: 9
128 is_required: false
128 is_required: false
129 field_format: date
129 field_format: date
130 default_value: ""
130 default_value: ""
131 editable: true
131 editable: true
General Comments 0
You need to be logged in to leave comments. Login now