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