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