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