##// END OF EJS Templates
Merged r6203 and r6204 from trunk....
Jean-Philippe Lang -
r6085:0527e8cedf01
parent child
Show More
@@ -1,877 +1,882
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
2 # Copyright (C) 2006-2011 Jean-Philippe Lang
3 #
3 #
4 # This program is free software; you can redistribute it and/or
4 # This program is free software; you can redistribute it and/or
5 # modify it under the terms of the GNU General Public License
5 # modify it under the terms of the GNU General Public License
6 # as published by the Free Software Foundation; either version 2
6 # as published by the Free Software Foundation; either version 2
7 # of the License, or (at your option) any later version.
7 # of the License, or (at your option) any later version.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU General Public License
14 # You should have received a copy of the GNU General Public License
15 # along with this program; if not, write to the Free Software
15 # along with this program; if not, write to the Free Software
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17
17
18 class Project < ActiveRecord::Base
18 class Project < ActiveRecord::Base
19 include Redmine::SafeAttributes
19 include Redmine::SafeAttributes
20
20
21 # Project statuses
21 # Project statuses
22 STATUS_ACTIVE = 1
22 STATUS_ACTIVE = 1
23 STATUS_ARCHIVED = 9
23 STATUS_ARCHIVED = 9
24
24
25 # Maximum length for project identifiers
25 # Maximum length for project identifiers
26 IDENTIFIER_MAX_LENGTH = 100
26 IDENTIFIER_MAX_LENGTH = 100
27
27
28 # Specific overidden Activities
28 # Specific overidden Activities
29 has_many :time_entry_activities
29 has_many :time_entry_activities
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
31 has_many :memberships, :class_name => 'Member'
31 has_many :memberships, :class_name => 'Member'
32 has_many :member_principals, :class_name => 'Member',
32 has_many :member_principals, :class_name => 'Member',
33 :include => :principal,
33 :include => :principal,
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
35 has_many :users, :through => :members
35 has_many :users, :through => :members
36 has_many :principals, :through => :member_principals, :source => :principal
36 has_many :principals, :through => :member_principals, :source => :principal
37
37
38 has_many :enabled_modules, :dependent => :delete_all
38 has_many :enabled_modules, :dependent => :delete_all
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
41 has_many :issue_changes, :through => :issues, :source => :journals
41 has_many :issue_changes, :through => :issues, :source => :journals
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
43 has_many :time_entries, :dependent => :delete_all
43 has_many :time_entries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
44 has_many :queries, :dependent => :delete_all
45 has_many :documents, :dependent => :destroy
45 has_many :documents, :dependent => :destroy
46 has_many :news, :dependent => :destroy, :include => :author
46 has_many :news, :dependent => :destroy, :include => :author
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
48 has_many :boards, :dependent => :destroy, :order => "position ASC"
49 has_one :repository, :dependent => :destroy
49 has_one :repository, :dependent => :destroy
50 has_many :changesets, :through => :repository
50 has_many :changesets, :through => :repository
51 has_one :wiki, :dependent => :destroy
51 has_one :wiki, :dependent => :destroy
52 # Custom field for the project issues
52 # Custom field for the project issues
53 has_and_belongs_to_many :issue_custom_fields,
53 has_and_belongs_to_many :issue_custom_fields,
54 :class_name => 'IssueCustomField',
54 :class_name => 'IssueCustomField',
55 :order => "#{CustomField.table_name}.position",
55 :order => "#{CustomField.table_name}.position",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
56 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
57 :association_foreign_key => 'custom_field_id'
57 :association_foreign_key => 'custom_field_id'
58
58
59 acts_as_nested_set :order => 'name', :dependent => :destroy
59 acts_as_nested_set :order => 'name', :dependent => :destroy
60 acts_as_attachable :view_permission => :view_files,
60 acts_as_attachable :view_permission => :view_files,
61 :delete_permission => :manage_files
61 :delete_permission => :manage_files
62
62
63 acts_as_customizable
63 acts_as_customizable
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
64 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
65 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
66 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
67 :author => nil
67 :author => nil
68
68
69 attr_protected :status
69 attr_protected :status
70
70
71 validates_presence_of :name, :identifier
71 validates_presence_of :name, :identifier
72 validates_uniqueness_of :identifier
72 validates_uniqueness_of :identifier
73 validates_associated :repository, :wiki
73 validates_associated :repository, :wiki
74 validates_length_of :name, :maximum => 255
74 validates_length_of :name, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
75 validates_length_of :homepage, :maximum => 255
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
77 # donwcase letters, digits, dashes but not digits only
77 # donwcase letters, digits, dashes but not digits only
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
79 # reserved words
79 # reserved words
80 validates_exclusion_of :identifier, :in => %w( new )
80 validates_exclusion_of :identifier, :in => %w( new )
81
81
82 before_destroy :delete_all_members
82 before_destroy :delete_all_members
83
83
84 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] } }
84 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] } }
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
86 named_scope :all_public, { :conditions => { :is_public => true } }
86 named_scope :all_public, { :conditions => { :is_public => true } }
87 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
87 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }}
88
88
89 def initialize(attributes = nil)
89 def initialize(attributes = nil)
90 super
90 super
91
91
92 initialized = (attributes || {}).stringify_keys
92 initialized = (attributes || {}).stringify_keys
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
93 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
94 self.identifier = Project.next_identifier
94 self.identifier = Project.next_identifier
95 end
95 end
96 if !initialized.key?('is_public')
96 if !initialized.key?('is_public')
97 self.is_public = Setting.default_projects_public?
97 self.is_public = Setting.default_projects_public?
98 end
98 end
99 if !initialized.key?('enabled_module_names')
99 if !initialized.key?('enabled_module_names')
100 self.enabled_module_names = Setting.default_projects_modules
100 self.enabled_module_names = Setting.default_projects_modules
101 end
101 end
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
102 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
103 self.trackers = Tracker.all
103 self.trackers = Tracker.all
104 end
104 end
105 end
105 end
106
106
107 def identifier=(identifier)
107 def identifier=(identifier)
108 super unless identifier_frozen?
108 super unless identifier_frozen?
109 end
109 end
110
110
111 def identifier_frozen?
111 def identifier_frozen?
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
112 errors[:identifier].nil? && !(new_record? || identifier.blank?)
113 end
113 end
114
114
115 # returns latest created projects
115 # returns latest created projects
116 # non public projects will be returned only if user is a member of those
116 # non public projects will be returned only if user is a member of those
117 def self.latest(user=nil, count=5)
117 def self.latest(user=nil, count=5)
118 visible(user).find(:all, :limit => count, :order => "created_on DESC")
118 visible(user).find(:all, :limit => count, :order => "created_on DESC")
119 end
119 end
120
120
121 # Returns true if the project is visible to +user+ or to the current user.
122 def visible?(user=User.current)
123 user.allowed_to?(:view_project, self)
124 end
125
121 def self.visible_by(user=nil)
126 def self.visible_by(user=nil)
122 ActiveSupport::Deprecation.warn "Project.visible_by is deprecated and will be removed in Redmine 1.3.0. Use Project.visible_condition instead."
127 ActiveSupport::Deprecation.warn "Project.visible_by is deprecated and will be removed in Redmine 1.3.0. Use Project.visible_condition instead."
123 visible_condition(user || User.current)
128 visible_condition(user || User.current)
124 end
129 end
125
130
126 # Returns a SQL conditions string used to find all projects visible by the specified user.
131 # Returns a SQL conditions string used to find all projects visible by the specified user.
127 #
132 #
128 # Examples:
133 # Examples:
129 # Project.visible_condition(admin) => "projects.status = 1"
134 # Project.visible_condition(admin) => "projects.status = 1"
130 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
135 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
131 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
136 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
132 def self.visible_condition(user, options={})
137 def self.visible_condition(user, options={})
133 allowed_to_condition(user, :view_project, options)
138 allowed_to_condition(user, :view_project, options)
134 end
139 end
135
140
136 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
141 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
137 #
142 #
138 # Valid options:
143 # Valid options:
139 # * :project => limit the condition to project
144 # * :project => limit the condition to project
140 # * :with_subprojects => limit the condition to project and its subprojects
145 # * :with_subprojects => limit the condition to project and its subprojects
141 # * :member => limit the condition to the user projects
146 # * :member => limit the condition to the user projects
142 def self.allowed_to_condition(user, permission, options={})
147 def self.allowed_to_condition(user, permission, options={})
143 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
148 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
144 if perm = Redmine::AccessControl.permission(permission)
149 if perm = Redmine::AccessControl.permission(permission)
145 unless perm.project_module.nil?
150 unless perm.project_module.nil?
146 # If the permission belongs to a project module, make sure the module is enabled
151 # If the permission belongs to a project module, make sure the module is enabled
147 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
152 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
148 end
153 end
149 end
154 end
150 if options[:project]
155 if options[:project]
151 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
156 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
152 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
157 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
153 base_statement = "(#{project_statement}) AND (#{base_statement})"
158 base_statement = "(#{project_statement}) AND (#{base_statement})"
154 end
159 end
155
160
156 if user.admin?
161 if user.admin?
157 base_statement
162 base_statement
158 else
163 else
159 statement_by_role = {}
164 statement_by_role = {}
160 unless options[:member]
165 unless options[:member]
161 role = user.logged? ? Role.non_member : Role.anonymous
166 role = user.logged? ? Role.non_member : Role.anonymous
162 if role.allowed_to?(permission)
167 if role.allowed_to?(permission)
163 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
168 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
164 end
169 end
165 end
170 end
166 if user.logged?
171 if user.logged?
167 user.projects_by_role.each do |role, projects|
172 user.projects_by_role.each do |role, projects|
168 if role.allowed_to?(permission)
173 if role.allowed_to?(permission)
169 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
174 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
170 end
175 end
171 end
176 end
172 end
177 end
173 if statement_by_role.empty?
178 if statement_by_role.empty?
174 "1=0"
179 "1=0"
175 else
180 else
176 if block_given?
181 if block_given?
177 statement_by_role.each do |role, statement|
182 statement_by_role.each do |role, statement|
178 if s = yield(role, user)
183 if s = yield(role, user)
179 statement_by_role[role] = "(#{statement} AND (#{s}))"
184 statement_by_role[role] = "(#{statement} AND (#{s}))"
180 end
185 end
181 end
186 end
182 end
187 end
183 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
188 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
184 end
189 end
185 end
190 end
186 end
191 end
187
192
188 # Returns the Systemwide and project specific activities
193 # Returns the Systemwide and project specific activities
189 def activities(include_inactive=false)
194 def activities(include_inactive=false)
190 if include_inactive
195 if include_inactive
191 return all_activities
196 return all_activities
192 else
197 else
193 return active_activities
198 return active_activities
194 end
199 end
195 end
200 end
196
201
197 # Will create a new Project specific Activity or update an existing one
202 # Will create a new Project specific Activity or update an existing one
198 #
203 #
199 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
204 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
200 # does not successfully save.
205 # does not successfully save.
201 def update_or_create_time_entry_activity(id, activity_hash)
206 def update_or_create_time_entry_activity(id, activity_hash)
202 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
207 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
203 self.create_time_entry_activity_if_needed(activity_hash)
208 self.create_time_entry_activity_if_needed(activity_hash)
204 else
209 else
205 activity = project.time_entry_activities.find_by_id(id.to_i)
210 activity = project.time_entry_activities.find_by_id(id.to_i)
206 activity.update_attributes(activity_hash) if activity
211 activity.update_attributes(activity_hash) if activity
207 end
212 end
208 end
213 end
209
214
210 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
215 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
211 #
216 #
212 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
217 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
213 # does not successfully save.
218 # does not successfully save.
214 def create_time_entry_activity_if_needed(activity)
219 def create_time_entry_activity_if_needed(activity)
215 if activity['parent_id']
220 if activity['parent_id']
216
221
217 parent_activity = TimeEntryActivity.find(activity['parent_id'])
222 parent_activity = TimeEntryActivity.find(activity['parent_id'])
218 activity['name'] = parent_activity.name
223 activity['name'] = parent_activity.name
219 activity['position'] = parent_activity.position
224 activity['position'] = parent_activity.position
220
225
221 if Enumeration.overridding_change?(activity, parent_activity)
226 if Enumeration.overridding_change?(activity, parent_activity)
222 project_activity = self.time_entry_activities.create(activity)
227 project_activity = self.time_entry_activities.create(activity)
223
228
224 if project_activity.new_record?
229 if project_activity.new_record?
225 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
230 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
226 else
231 else
227 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
232 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
228 end
233 end
229 end
234 end
230 end
235 end
231 end
236 end
232
237
233 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
238 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
234 #
239 #
235 # Examples:
240 # Examples:
236 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
241 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
237 # project.project_condition(false) => "projects.id = 1"
242 # project.project_condition(false) => "projects.id = 1"
238 def project_condition(with_subprojects)
243 def project_condition(with_subprojects)
239 cond = "#{Project.table_name}.id = #{id}"
244 cond = "#{Project.table_name}.id = #{id}"
240 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
245 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
241 cond
246 cond
242 end
247 end
243
248
244 def self.find(*args)
249 def self.find(*args)
245 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
250 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
246 project = find_by_identifier(*args)
251 project = find_by_identifier(*args)
247 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
252 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
248 project
253 project
249 else
254 else
250 super
255 super
251 end
256 end
252 end
257 end
253
258
254 def to_param
259 def to_param
255 # id is used for projects with a numeric identifier (compatibility)
260 # id is used for projects with a numeric identifier (compatibility)
256 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
261 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
257 end
262 end
258
263
259 def active?
264 def active?
260 self.status == STATUS_ACTIVE
265 self.status == STATUS_ACTIVE
261 end
266 end
262
267
263 def archived?
268 def archived?
264 self.status == STATUS_ARCHIVED
269 self.status == STATUS_ARCHIVED
265 end
270 end
266
271
267 # Archives the project and its descendants
272 # Archives the project and its descendants
268 def archive
273 def archive
269 # Check that there is no issue of a non descendant project that is assigned
274 # Check that there is no issue of a non descendant project that is assigned
270 # to one of the project or descendant versions
275 # to one of the project or descendant versions
271 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
276 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
272 if v_ids.any? && Issue.find(:first, :include => :project,
277 if v_ids.any? && Issue.find(:first, :include => :project,
273 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
278 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
274 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
279 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
275 return false
280 return false
276 end
281 end
277 Project.transaction do
282 Project.transaction do
278 archive!
283 archive!
279 end
284 end
280 true
285 true
281 end
286 end
282
287
283 # Unarchives the project
288 # Unarchives the project
284 # All its ancestors must be active
289 # All its ancestors must be active
285 def unarchive
290 def unarchive
286 return false if ancestors.detect {|a| !a.active?}
291 return false if ancestors.detect {|a| !a.active?}
287 update_attribute :status, STATUS_ACTIVE
292 update_attribute :status, STATUS_ACTIVE
288 end
293 end
289
294
290 # Returns an array of projects the project can be moved to
295 # Returns an array of projects the project can be moved to
291 # by the current user
296 # by the current user
292 def allowed_parents
297 def allowed_parents
293 return @allowed_parents if @allowed_parents
298 return @allowed_parents if @allowed_parents
294 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
299 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
295 @allowed_parents = @allowed_parents - self_and_descendants
300 @allowed_parents = @allowed_parents - self_and_descendants
296 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
301 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
297 @allowed_parents << nil
302 @allowed_parents << nil
298 end
303 end
299 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
304 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
300 @allowed_parents << parent
305 @allowed_parents << parent
301 end
306 end
302 @allowed_parents
307 @allowed_parents
303 end
308 end
304
309
305 # Sets the parent of the project with authorization check
310 # Sets the parent of the project with authorization check
306 def set_allowed_parent!(p)
311 def set_allowed_parent!(p)
307 unless p.nil? || p.is_a?(Project)
312 unless p.nil? || p.is_a?(Project)
308 if p.to_s.blank?
313 if p.to_s.blank?
309 p = nil
314 p = nil
310 else
315 else
311 p = Project.find_by_id(p)
316 p = Project.find_by_id(p)
312 return false unless p
317 return false unless p
313 end
318 end
314 end
319 end
315 if p.nil?
320 if p.nil?
316 if !new_record? && allowed_parents.empty?
321 if !new_record? && allowed_parents.empty?
317 return false
322 return false
318 end
323 end
319 elsif !allowed_parents.include?(p)
324 elsif !allowed_parents.include?(p)
320 return false
325 return false
321 end
326 end
322 set_parent!(p)
327 set_parent!(p)
323 end
328 end
324
329
325 # Sets the parent of the project
330 # Sets the parent of the project
326 # Argument can be either a Project, a String, a Fixnum or nil
331 # Argument can be either a Project, a String, a Fixnum or nil
327 def set_parent!(p)
332 def set_parent!(p)
328 unless p.nil? || p.is_a?(Project)
333 unless p.nil? || p.is_a?(Project)
329 if p.to_s.blank?
334 if p.to_s.blank?
330 p = nil
335 p = nil
331 else
336 else
332 p = Project.find_by_id(p)
337 p = Project.find_by_id(p)
333 return false unless p
338 return false unless p
334 end
339 end
335 end
340 end
336 if p == parent && !p.nil?
341 if p == parent && !p.nil?
337 # Nothing to do
342 # Nothing to do
338 true
343 true
339 elsif p.nil? || (p.active? && move_possible?(p))
344 elsif p.nil? || (p.active? && move_possible?(p))
340 # Insert the project so that target's children or root projects stay alphabetically sorted
345 # Insert the project so that target's children or root projects stay alphabetically sorted
341 sibs = (p.nil? ? self.class.roots : p.children)
346 sibs = (p.nil? ? self.class.roots : p.children)
342 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
347 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
343 if to_be_inserted_before
348 if to_be_inserted_before
344 move_to_left_of(to_be_inserted_before)
349 move_to_left_of(to_be_inserted_before)
345 elsif p.nil?
350 elsif p.nil?
346 if sibs.empty?
351 if sibs.empty?
347 # move_to_root adds the project in first (ie. left) position
352 # move_to_root adds the project in first (ie. left) position
348 move_to_root
353 move_to_root
349 else
354 else
350 move_to_right_of(sibs.last) unless self == sibs.last
355 move_to_right_of(sibs.last) unless self == sibs.last
351 end
356 end
352 else
357 else
353 # move_to_child_of adds the project in last (ie.right) position
358 # move_to_child_of adds the project in last (ie.right) position
354 move_to_child_of(p)
359 move_to_child_of(p)
355 end
360 end
356 Issue.update_versions_from_hierarchy_change(self)
361 Issue.update_versions_from_hierarchy_change(self)
357 true
362 true
358 else
363 else
359 # Can not move to the given target
364 # Can not move to the given target
360 false
365 false
361 end
366 end
362 end
367 end
363
368
364 # Returns an array of the trackers used by the project and its active sub projects
369 # Returns an array of the trackers used by the project and its active sub projects
365 def rolled_up_trackers
370 def rolled_up_trackers
366 @rolled_up_trackers ||=
371 @rolled_up_trackers ||=
367 Tracker.find(:all, :joins => :projects,
372 Tracker.find(:all, :joins => :projects,
368 :select => "DISTINCT #{Tracker.table_name}.*",
373 :select => "DISTINCT #{Tracker.table_name}.*",
369 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
374 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
370 :order => "#{Tracker.table_name}.position")
375 :order => "#{Tracker.table_name}.position")
371 end
376 end
372
377
373 # Closes open and locked project versions that are completed
378 # Closes open and locked project versions that are completed
374 def close_completed_versions
379 def close_completed_versions
375 Version.transaction do
380 Version.transaction do
376 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
381 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
377 if version.completed?
382 if version.completed?
378 version.update_attribute(:status, 'closed')
383 version.update_attribute(:status, 'closed')
379 end
384 end
380 end
385 end
381 end
386 end
382 end
387 end
383
388
384 # Returns a scope of the Versions on subprojects
389 # Returns a scope of the Versions on subprojects
385 def rolled_up_versions
390 def rolled_up_versions
386 @rolled_up_versions ||=
391 @rolled_up_versions ||=
387 Version.scoped(:include => :project,
392 Version.scoped(:include => :project,
388 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
393 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
389 end
394 end
390
395
391 # Returns a scope of the Versions used by the project
396 # Returns a scope of the Versions used by the project
392 def shared_versions
397 def shared_versions
393 @shared_versions ||= begin
398 @shared_versions ||= begin
394 r = root? ? self : root
399 r = root? ? self : root
395 Version.scoped(:include => :project,
400 Version.scoped(:include => :project,
396 :conditions => "#{Project.table_name}.id = #{id}" +
401 :conditions => "#{Project.table_name}.id = #{id}" +
397 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
402 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
398 " #{Version.table_name}.sharing = 'system'" +
403 " #{Version.table_name}.sharing = 'system'" +
399 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
404 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
400 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
405 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
401 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
406 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
402 "))")
407 "))")
403 end
408 end
404 end
409 end
405
410
406 # Returns a hash of project users grouped by role
411 # Returns a hash of project users grouped by role
407 def users_by_role
412 def users_by_role
408 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
413 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
409 m.roles.each do |r|
414 m.roles.each do |r|
410 h[r] ||= []
415 h[r] ||= []
411 h[r] << m.user
416 h[r] << m.user
412 end
417 end
413 h
418 h
414 end
419 end
415 end
420 end
416
421
417 # Deletes all project's members
422 # Deletes all project's members
418 def delete_all_members
423 def delete_all_members
419 me, mr = Member.table_name, MemberRole.table_name
424 me, mr = Member.table_name, MemberRole.table_name
420 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
425 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
421 Member.delete_all(['project_id = ?', id])
426 Member.delete_all(['project_id = ?', id])
422 end
427 end
423
428
424 # Users issues can be assigned to
429 # Users issues can be assigned to
425 def assignable_users
430 def assignable_users
426 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
431 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
427 end
432 end
428
433
429 # Returns the mail adresses of users that should be always notified on project events
434 # Returns the mail adresses of users that should be always notified on project events
430 def recipients
435 def recipients
431 notified_users.collect {|user| user.mail}
436 notified_users.collect {|user| user.mail}
432 end
437 end
433
438
434 # Returns the users that should be notified on project events
439 # Returns the users that should be notified on project events
435 def notified_users
440 def notified_users
436 # TODO: User part should be extracted to User#notify_about?
441 # TODO: User part should be extracted to User#notify_about?
437 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
442 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
438 end
443 end
439
444
440 # Returns an array of all custom fields enabled for project issues
445 # Returns an array of all custom fields enabled for project issues
441 # (explictly associated custom fields and custom fields enabled for all projects)
446 # (explictly associated custom fields and custom fields enabled for all projects)
442 def all_issue_custom_fields
447 def all_issue_custom_fields
443 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
448 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
444 end
449 end
445
450
446 # Returns an array of all custom fields enabled for project time entries
451 # Returns an array of all custom fields enabled for project time entries
447 # (explictly associated custom fields and custom fields enabled for all projects)
452 # (explictly associated custom fields and custom fields enabled for all projects)
448 def all_time_entry_custom_fields
453 def all_time_entry_custom_fields
449 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
454 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
450 end
455 end
451
456
452 def project
457 def project
453 self
458 self
454 end
459 end
455
460
456 def <=>(project)
461 def <=>(project)
457 name.downcase <=> project.name.downcase
462 name.downcase <=> project.name.downcase
458 end
463 end
459
464
460 def to_s
465 def to_s
461 name
466 name
462 end
467 end
463
468
464 # Returns a short description of the projects (first lines)
469 # Returns a short description of the projects (first lines)
465 def short_description(length = 255)
470 def short_description(length = 255)
466 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
471 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
467 end
472 end
468
473
469 def css_classes
474 def css_classes
470 s = 'project'
475 s = 'project'
471 s << ' root' if root?
476 s << ' root' if root?
472 s << ' child' if child?
477 s << ' child' if child?
473 s << (leaf? ? ' leaf' : ' parent')
478 s << (leaf? ? ' leaf' : ' parent')
474 s
479 s
475 end
480 end
476
481
477 # The earliest start date of a project, based on it's issues and versions
482 # The earliest start date of a project, based on it's issues and versions
478 def start_date
483 def start_date
479 [
484 [
480 issues.minimum('start_date'),
485 issues.minimum('start_date'),
481 shared_versions.collect(&:effective_date),
486 shared_versions.collect(&:effective_date),
482 shared_versions.collect(&:start_date)
487 shared_versions.collect(&:start_date)
483 ].flatten.compact.min
488 ].flatten.compact.min
484 end
489 end
485
490
486 # The latest due date of an issue or version
491 # The latest due date of an issue or version
487 def due_date
492 def due_date
488 [
493 [
489 issues.maximum('due_date'),
494 issues.maximum('due_date'),
490 shared_versions.collect(&:effective_date),
495 shared_versions.collect(&:effective_date),
491 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
496 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
492 ].flatten.compact.max
497 ].flatten.compact.max
493 end
498 end
494
499
495 def overdue?
500 def overdue?
496 active? && !due_date.nil? && (due_date < Date.today)
501 active? && !due_date.nil? && (due_date < Date.today)
497 end
502 end
498
503
499 # Returns the percent completed for this project, based on the
504 # Returns the percent completed for this project, based on the
500 # progress on it's versions.
505 # progress on it's versions.
501 def completed_percent(options={:include_subprojects => false})
506 def completed_percent(options={:include_subprojects => false})
502 if options.delete(:include_subprojects)
507 if options.delete(:include_subprojects)
503 total = self_and_descendants.collect(&:completed_percent).sum
508 total = self_and_descendants.collect(&:completed_percent).sum
504
509
505 total / self_and_descendants.count
510 total / self_and_descendants.count
506 else
511 else
507 if versions.count > 0
512 if versions.count > 0
508 total = versions.collect(&:completed_pourcent).sum
513 total = versions.collect(&:completed_pourcent).sum
509
514
510 total / versions.count
515 total / versions.count
511 else
516 else
512 100
517 100
513 end
518 end
514 end
519 end
515 end
520 end
516
521
517 # Return true if this project is allowed to do the specified action.
522 # Return true if this project is allowed to do the specified action.
518 # action can be:
523 # action can be:
519 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
524 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
520 # * a permission Symbol (eg. :edit_project)
525 # * a permission Symbol (eg. :edit_project)
521 def allows_to?(action)
526 def allows_to?(action)
522 if action.is_a? Hash
527 if action.is_a? Hash
523 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
528 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
524 else
529 else
525 allowed_permissions.include? action
530 allowed_permissions.include? action
526 end
531 end
527 end
532 end
528
533
529 def module_enabled?(module_name)
534 def module_enabled?(module_name)
530 module_name = module_name.to_s
535 module_name = module_name.to_s
531 enabled_modules.detect {|m| m.name == module_name}
536 enabled_modules.detect {|m| m.name == module_name}
532 end
537 end
533
538
534 def enabled_module_names=(module_names)
539 def enabled_module_names=(module_names)
535 if module_names && module_names.is_a?(Array)
540 if module_names && module_names.is_a?(Array)
536 module_names = module_names.collect(&:to_s).reject(&:blank?)
541 module_names = module_names.collect(&:to_s).reject(&:blank?)
537 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
542 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
538 else
543 else
539 enabled_modules.clear
544 enabled_modules.clear
540 end
545 end
541 end
546 end
542
547
543 # Returns an array of the enabled modules names
548 # Returns an array of the enabled modules names
544 def enabled_module_names
549 def enabled_module_names
545 enabled_modules.collect(&:name)
550 enabled_modules.collect(&:name)
546 end
551 end
547
552
548 # Enable a specific module
553 # Enable a specific module
549 #
554 #
550 # Examples:
555 # Examples:
551 # project.enable_module!(:issue_tracking)
556 # project.enable_module!(:issue_tracking)
552 # project.enable_module!("issue_tracking")
557 # project.enable_module!("issue_tracking")
553 def enable_module!(name)
558 def enable_module!(name)
554 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
559 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
555 end
560 end
556
561
557 # Disable a module if it exists
562 # Disable a module if it exists
558 #
563 #
559 # Examples:
564 # Examples:
560 # project.disable_module!(:issue_tracking)
565 # project.disable_module!(:issue_tracking)
561 # project.disable_module!("issue_tracking")
566 # project.disable_module!("issue_tracking")
562 # project.disable_module!(project.enabled_modules.first)
567 # project.disable_module!(project.enabled_modules.first)
563 def disable_module!(target)
568 def disable_module!(target)
564 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
569 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
565 target.destroy unless target.blank?
570 target.destroy unless target.blank?
566 end
571 end
567
572
568 safe_attributes 'name',
573 safe_attributes 'name',
569 'description',
574 'description',
570 'homepage',
575 'homepage',
571 'is_public',
576 'is_public',
572 'identifier',
577 'identifier',
573 'custom_field_values',
578 'custom_field_values',
574 'custom_fields',
579 'custom_fields',
575 'tracker_ids',
580 'tracker_ids',
576 'issue_custom_field_ids'
581 'issue_custom_field_ids'
577
582
578 safe_attributes 'enabled_module_names',
583 safe_attributes 'enabled_module_names',
579 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
584 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
580
585
581 # Returns an array of projects that are in this project's hierarchy
586 # Returns an array of projects that are in this project's hierarchy
582 #
587 #
583 # Example: parents, children, siblings
588 # Example: parents, children, siblings
584 def hierarchy
589 def hierarchy
585 parents = project.self_and_ancestors || []
590 parents = project.self_and_ancestors || []
586 descendants = project.descendants || []
591 descendants = project.descendants || []
587 project_hierarchy = parents | descendants # Set union
592 project_hierarchy = parents | descendants # Set union
588 end
593 end
589
594
590 # Returns an auto-generated project identifier based on the last identifier used
595 # Returns an auto-generated project identifier based on the last identifier used
591 def self.next_identifier
596 def self.next_identifier
592 p = Project.find(:first, :order => 'created_on DESC')
597 p = Project.find(:first, :order => 'created_on DESC')
593 p.nil? ? nil : p.identifier.to_s.succ
598 p.nil? ? nil : p.identifier.to_s.succ
594 end
599 end
595
600
596 # Copies and saves the Project instance based on the +project+.
601 # Copies and saves the Project instance based on the +project+.
597 # Duplicates the source project's:
602 # Duplicates the source project's:
598 # * Wiki
603 # * Wiki
599 # * Versions
604 # * Versions
600 # * Categories
605 # * Categories
601 # * Issues
606 # * Issues
602 # * Members
607 # * Members
603 # * Queries
608 # * Queries
604 #
609 #
605 # Accepts an +options+ argument to specify what to copy
610 # Accepts an +options+ argument to specify what to copy
606 #
611 #
607 # Examples:
612 # Examples:
608 # project.copy(1) # => copies everything
613 # project.copy(1) # => copies everything
609 # project.copy(1, :only => 'members') # => copies members only
614 # project.copy(1, :only => 'members') # => copies members only
610 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
615 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
611 def copy(project, options={})
616 def copy(project, options={})
612 project = project.is_a?(Project) ? project : Project.find(project)
617 project = project.is_a?(Project) ? project : Project.find(project)
613
618
614 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
619 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
615 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
620 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
616
621
617 Project.transaction do
622 Project.transaction do
618 if save
623 if save
619 reload
624 reload
620 to_be_copied.each do |name|
625 to_be_copied.each do |name|
621 send "copy_#{name}", project
626 send "copy_#{name}", project
622 end
627 end
623 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
628 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
624 save
629 save
625 end
630 end
626 end
631 end
627 end
632 end
628
633
629
634
630 # Copies +project+ and returns the new instance. This will not save
635 # Copies +project+ and returns the new instance. This will not save
631 # the copy
636 # the copy
632 def self.copy_from(project)
637 def self.copy_from(project)
633 begin
638 begin
634 project = project.is_a?(Project) ? project : Project.find(project)
639 project = project.is_a?(Project) ? project : Project.find(project)
635 if project
640 if project
636 # clear unique attributes
641 # clear unique attributes
637 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
642 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
638 copy = Project.new(attributes)
643 copy = Project.new(attributes)
639 copy.enabled_modules = project.enabled_modules
644 copy.enabled_modules = project.enabled_modules
640 copy.trackers = project.trackers
645 copy.trackers = project.trackers
641 copy.custom_values = project.custom_values.collect {|v| v.clone}
646 copy.custom_values = project.custom_values.collect {|v| v.clone}
642 copy.issue_custom_fields = project.issue_custom_fields
647 copy.issue_custom_fields = project.issue_custom_fields
643 return copy
648 return copy
644 else
649 else
645 return nil
650 return nil
646 end
651 end
647 rescue ActiveRecord::RecordNotFound
652 rescue ActiveRecord::RecordNotFound
648 return nil
653 return nil
649 end
654 end
650 end
655 end
651
656
652 # Yields the given block for each project with its level in the tree
657 # Yields the given block for each project with its level in the tree
653 def self.project_tree(projects, &block)
658 def self.project_tree(projects, &block)
654 ancestors = []
659 ancestors = []
655 projects.sort_by(&:lft).each do |project|
660 projects.sort_by(&:lft).each do |project|
656 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
661 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
657 ancestors.pop
662 ancestors.pop
658 end
663 end
659 yield project, ancestors.size
664 yield project, ancestors.size
660 ancestors << project
665 ancestors << project
661 end
666 end
662 end
667 end
663
668
664 private
669 private
665
670
666 # Copies wiki from +project+
671 # Copies wiki from +project+
667 def copy_wiki(project)
672 def copy_wiki(project)
668 # Check that the source project has a wiki first
673 # Check that the source project has a wiki first
669 unless project.wiki.nil?
674 unless project.wiki.nil?
670 self.wiki ||= Wiki.new
675 self.wiki ||= Wiki.new
671 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
676 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
672 wiki_pages_map = {}
677 wiki_pages_map = {}
673 project.wiki.pages.each do |page|
678 project.wiki.pages.each do |page|
674 # Skip pages without content
679 # Skip pages without content
675 next if page.content.nil?
680 next if page.content.nil?
676 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
681 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
677 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
682 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
678 new_wiki_page.content = new_wiki_content
683 new_wiki_page.content = new_wiki_content
679 wiki.pages << new_wiki_page
684 wiki.pages << new_wiki_page
680 wiki_pages_map[page.id] = new_wiki_page
685 wiki_pages_map[page.id] = new_wiki_page
681 end
686 end
682 wiki.save
687 wiki.save
683 # Reproduce page hierarchy
688 # Reproduce page hierarchy
684 project.wiki.pages.each do |page|
689 project.wiki.pages.each do |page|
685 if page.parent_id && wiki_pages_map[page.id]
690 if page.parent_id && wiki_pages_map[page.id]
686 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
691 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
687 wiki_pages_map[page.id].save
692 wiki_pages_map[page.id].save
688 end
693 end
689 end
694 end
690 end
695 end
691 end
696 end
692
697
693 # Copies versions from +project+
698 # Copies versions from +project+
694 def copy_versions(project)
699 def copy_versions(project)
695 project.versions.each do |version|
700 project.versions.each do |version|
696 new_version = Version.new
701 new_version = Version.new
697 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
702 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
698 self.versions << new_version
703 self.versions << new_version
699 end
704 end
700 end
705 end
701
706
702 # Copies issue categories from +project+
707 # Copies issue categories from +project+
703 def copy_issue_categories(project)
708 def copy_issue_categories(project)
704 project.issue_categories.each do |issue_category|
709 project.issue_categories.each do |issue_category|
705 new_issue_category = IssueCategory.new
710 new_issue_category = IssueCategory.new
706 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
711 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
707 self.issue_categories << new_issue_category
712 self.issue_categories << new_issue_category
708 end
713 end
709 end
714 end
710
715
711 # Copies issues from +project+
716 # Copies issues from +project+
712 # Note: issues assigned to a closed version won't be copied due to validation rules
717 # Note: issues assigned to a closed version won't be copied due to validation rules
713 def copy_issues(project)
718 def copy_issues(project)
714 # Stores the source issue id as a key and the copied issues as the
719 # Stores the source issue id as a key and the copied issues as the
715 # value. Used to map the two togeather for issue relations.
720 # value. Used to map the two togeather for issue relations.
716 issues_map = {}
721 issues_map = {}
717
722
718 # Get issues sorted by root_id, lft so that parent issues
723 # Get issues sorted by root_id, lft so that parent issues
719 # get copied before their children
724 # get copied before their children
720 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
725 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
721 new_issue = Issue.new
726 new_issue = Issue.new
722 new_issue.copy_from(issue)
727 new_issue.copy_from(issue)
723 new_issue.project = self
728 new_issue.project = self
724 # Reassign fixed_versions by name, since names are unique per
729 # Reassign fixed_versions by name, since names are unique per
725 # project and the versions for self are not yet saved
730 # project and the versions for self are not yet saved
726 if issue.fixed_version
731 if issue.fixed_version
727 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
732 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
728 end
733 end
729 # Reassign the category by name, since names are unique per
734 # Reassign the category by name, since names are unique per
730 # project and the categories for self are not yet saved
735 # project and the categories for self are not yet saved
731 if issue.category
736 if issue.category
732 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
737 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
733 end
738 end
734 # Parent issue
739 # Parent issue
735 if issue.parent_id
740 if issue.parent_id
736 if copied_parent = issues_map[issue.parent_id]
741 if copied_parent = issues_map[issue.parent_id]
737 new_issue.parent_issue_id = copied_parent.id
742 new_issue.parent_issue_id = copied_parent.id
738 end
743 end
739 end
744 end
740
745
741 self.issues << new_issue
746 self.issues << new_issue
742 if new_issue.new_record?
747 if new_issue.new_record?
743 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
748 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
744 else
749 else
745 issues_map[issue.id] = new_issue unless new_issue.new_record?
750 issues_map[issue.id] = new_issue unless new_issue.new_record?
746 end
751 end
747 end
752 end
748
753
749 # Relations after in case issues related each other
754 # Relations after in case issues related each other
750 project.issues.each do |issue|
755 project.issues.each do |issue|
751 new_issue = issues_map[issue.id]
756 new_issue = issues_map[issue.id]
752 unless new_issue
757 unless new_issue
753 # Issue was not copied
758 # Issue was not copied
754 next
759 next
755 end
760 end
756
761
757 # Relations
762 # Relations
758 issue.relations_from.each do |source_relation|
763 issue.relations_from.each do |source_relation|
759 new_issue_relation = IssueRelation.new
764 new_issue_relation = IssueRelation.new
760 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
765 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
761 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
766 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
762 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
767 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
763 new_issue_relation.issue_to = source_relation.issue_to
768 new_issue_relation.issue_to = source_relation.issue_to
764 end
769 end
765 new_issue.relations_from << new_issue_relation
770 new_issue.relations_from << new_issue_relation
766 end
771 end
767
772
768 issue.relations_to.each do |source_relation|
773 issue.relations_to.each do |source_relation|
769 new_issue_relation = IssueRelation.new
774 new_issue_relation = IssueRelation.new
770 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
775 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
771 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
776 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
772 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
777 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
773 new_issue_relation.issue_from = source_relation.issue_from
778 new_issue_relation.issue_from = source_relation.issue_from
774 end
779 end
775 new_issue.relations_to << new_issue_relation
780 new_issue.relations_to << new_issue_relation
776 end
781 end
777 end
782 end
778 end
783 end
779
784
780 # Copies members from +project+
785 # Copies members from +project+
781 def copy_members(project)
786 def copy_members(project)
782 # Copy users first, then groups to handle members with inherited and given roles
787 # Copy users first, then groups to handle members with inherited and given roles
783 members_to_copy = []
788 members_to_copy = []
784 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
789 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
785 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
790 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
786
791
787 members_to_copy.each do |member|
792 members_to_copy.each do |member|
788 new_member = Member.new
793 new_member = Member.new
789 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
794 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
790 # only copy non inherited roles
795 # only copy non inherited roles
791 # inherited roles will be added when copying the group membership
796 # inherited roles will be added when copying the group membership
792 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
797 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
793 next if role_ids.empty?
798 next if role_ids.empty?
794 new_member.role_ids = role_ids
799 new_member.role_ids = role_ids
795 new_member.project = self
800 new_member.project = self
796 self.members << new_member
801 self.members << new_member
797 end
802 end
798 end
803 end
799
804
800 # Copies queries from +project+
805 # Copies queries from +project+
801 def copy_queries(project)
806 def copy_queries(project)
802 project.queries.each do |query|
807 project.queries.each do |query|
803 new_query = Query.new
808 new_query = Query.new
804 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
809 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
805 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
810 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
806 new_query.project = self
811 new_query.project = self
807 self.queries << new_query
812 self.queries << new_query
808 end
813 end
809 end
814 end
810
815
811 # Copies boards from +project+
816 # Copies boards from +project+
812 def copy_boards(project)
817 def copy_boards(project)
813 project.boards.each do |board|
818 project.boards.each do |board|
814 new_board = Board.new
819 new_board = Board.new
815 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
820 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
816 new_board.project = self
821 new_board.project = self
817 self.boards << new_board
822 self.boards << new_board
818 end
823 end
819 end
824 end
820
825
821 def allowed_permissions
826 def allowed_permissions
822 @allowed_permissions ||= begin
827 @allowed_permissions ||= begin
823 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
828 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
824 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
829 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
825 end
830 end
826 end
831 end
827
832
828 def allowed_actions
833 def allowed_actions
829 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
834 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
830 end
835 end
831
836
832 # Returns all the active Systemwide and project specific activities
837 # Returns all the active Systemwide and project specific activities
833 def active_activities
838 def active_activities
834 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
839 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
835
840
836 if overridden_activity_ids.empty?
841 if overridden_activity_ids.empty?
837 return TimeEntryActivity.shared.active
842 return TimeEntryActivity.shared.active
838 else
843 else
839 return system_activities_and_project_overrides
844 return system_activities_and_project_overrides
840 end
845 end
841 end
846 end
842
847
843 # Returns all the Systemwide and project specific activities
848 # Returns all the Systemwide and project specific activities
844 # (inactive and active)
849 # (inactive and active)
845 def all_activities
850 def all_activities
846 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
851 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
847
852
848 if overridden_activity_ids.empty?
853 if overridden_activity_ids.empty?
849 return TimeEntryActivity.shared
854 return TimeEntryActivity.shared
850 else
855 else
851 return system_activities_and_project_overrides(true)
856 return system_activities_and_project_overrides(true)
852 end
857 end
853 end
858 end
854
859
855 # Returns the systemwide active activities merged with the project specific overrides
860 # Returns the systemwide active activities merged with the project specific overrides
856 def system_activities_and_project_overrides(include_inactive=false)
861 def system_activities_and_project_overrides(include_inactive=false)
857 if include_inactive
862 if include_inactive
858 return TimeEntryActivity.shared.
863 return TimeEntryActivity.shared.
859 find(:all,
864 find(:all,
860 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
865 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
861 self.time_entry_activities
866 self.time_entry_activities
862 else
867 else
863 return TimeEntryActivity.shared.active.
868 return TimeEntryActivity.shared.active.
864 find(:all,
869 find(:all,
865 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
870 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
866 self.time_entry_activities.active
871 self.time_entry_activities.active
867 end
872 end
868 end
873 end
869
874
870 # Archives subprojects recursively
875 # Archives subprojects recursively
871 def archive!
876 def archive!
872 children.each do |subproject|
877 children.each do |subproject|
873 subproject.send :archive!
878 subproject.send :archive!
874 end
879 end
875 update_attribute :status, STATUS_ARCHIVED
880 update_attribute :status, STATUS_ARCHIVED
876 end
881 end
877 end
882 end
@@ -1,16 +1,16
1 api.array :projects, api_meta(:total_count => @project_count, :offset => @offset, :limit => @limit) do
1 api.array :projects, api_meta(:total_count => @project_count, :offset => @offset, :limit => @limit) do
2 @projects.each do |project|
2 @projects.each do |project|
3 api.project do
3 api.project do
4 api.id project.id
4 api.id project.id
5 api.name project.name
5 api.name project.name
6 api.identifier project.identifier
6 api.identifier project.identifier
7 api.description project.description
7 api.description project.description
8 api.parent(:id => project.parent_id, :name => project.parent.name) unless project.parent.nil?
8 api.parent(:id => project.parent.id, :name => project.parent.name) if project.parent && project.parent.visible?
9
9
10 render_api_custom_values project.visible_custom_field_values, api
10 render_api_custom_values project.visible_custom_field_values, api
11
11
12 api.created_on project.created_on
12 api.created_on project.created_on
13 api.updated_on project.updated_on
13 api.updated_on project.updated_on
14 end
14 end
15 end
15 end
16 end
16 end
@@ -1,18 +1,19
1 api.project do
1 api.project do
2 api.id @project.id
2 api.id @project.id
3 api.name @project.name
3 api.name @project.name
4 api.identifier @project.identifier
4 api.identifier @project.identifier
5 api.description @project.description
5 api.description @project.description
6 api.homepage @project.homepage
6 api.homepage @project.homepage
7 api.parent(:id => @project.parent.id, :name => @project.parent.name) if @project.parent && @project.parent.visible?
7
8
8 render_api_custom_values @project.visible_custom_field_values, api
9 render_api_custom_values @project.visible_custom_field_values, api
9
10
10 api.created_on @project.created_on
11 api.created_on @project.created_on
11 api.updated_on @project.updated_on
12 api.updated_on @project.updated_on
12
13
13 api.array :trackers do
14 api.array :trackers do
14 @project.trackers.each do |tracker|
15 @project.trackers.each do |tracker|
15 api.tracker(:id => tracker.id, :name => tracker.name)
16 api.tracker(:id => tracker.id, :name => tracker.name)
16 end
17 end
17 end if include_in_api_response?('trackers')
18 end if include_in_api_response?('trackers')
18 end
19 end
General Comments 0
You need to be logged in to leave comments. Login now