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