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