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