##// END OF EJS Templates
Error message when editing a child project without add project/subprojects permissions (#20282)....
Jean-Philippe Lang -
r14237:64a80066bd86
parent child
Show More
@@ -1,1040 +1,1042
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 if new_record?
529 if new_record?
530 @all_issue_custom_fields ||= IssueCustomField.
530 @all_issue_custom_fields ||= IssueCustomField.
531 sorted.
531 sorted.
532 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
532 where("is_for_all = ? OR id IN (?)", true, issue_custom_field_ids)
533 else
533 else
534 @all_issue_custom_fields ||= IssueCustomField.
534 @all_issue_custom_fields ||= IssueCustomField.
535 sorted.
535 sorted.
536 where("is_for_all = ? OR id IN (SELECT DISTINCT cfp.custom_field_id" +
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" +
537 " FROM #{table_name_prefix}custom_fields_projects#{table_name_suffix} cfp" +
538 " WHERE cfp.project_id = ?)", true, id)
538 " WHERE cfp.project_id = ?)", true, id)
539 end
539 end
540 end
540 end
541
541
542 def project
542 def project
543 self
543 self
544 end
544 end
545
545
546 def <=>(project)
546 def <=>(project)
547 name.casecmp(project.name)
547 name.casecmp(project.name)
548 end
548 end
549
549
550 def to_s
550 def to_s
551 name
551 name
552 end
552 end
553
553
554 # Returns a short description of the projects (first lines)
554 # Returns a short description of the projects (first lines)
555 def short_description(length = 255)
555 def short_description(length = 255)
556 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
556 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
557 end
557 end
558
558
559 def css_classes
559 def css_classes
560 s = 'project'
560 s = 'project'
561 s << ' root' if root?
561 s << ' root' if root?
562 s << ' child' if child?
562 s << ' child' if child?
563 s << (leaf? ? ' leaf' : ' parent')
563 s << (leaf? ? ' leaf' : ' parent')
564 unless active?
564 unless active?
565 if archived?
565 if archived?
566 s << ' archived'
566 s << ' archived'
567 else
567 else
568 s << ' closed'
568 s << ' closed'
569 end
569 end
570 end
570 end
571 s
571 s
572 end
572 end
573
573
574 # 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
575 def start_date
575 def start_date
576 @start_date ||= [
576 @start_date ||= [
577 issues.minimum('start_date'),
577 issues.minimum('start_date'),
578 shared_versions.minimum('effective_date'),
578 shared_versions.minimum('effective_date'),
579 Issue.fixed_version(shared_versions).minimum('start_date')
579 Issue.fixed_version(shared_versions).minimum('start_date')
580 ].compact.min
580 ].compact.min
581 end
581 end
582
582
583 # The latest due date of an issue or version
583 # The latest due date of an issue or version
584 def due_date
584 def due_date
585 @due_date ||= [
585 @due_date ||= [
586 issues.maximum('due_date'),
586 issues.maximum('due_date'),
587 shared_versions.maximum('effective_date'),
587 shared_versions.maximum('effective_date'),
588 Issue.fixed_version(shared_versions).maximum('due_date')
588 Issue.fixed_version(shared_versions).maximum('due_date')
589 ].compact.max
589 ].compact.max
590 end
590 end
591
591
592 def overdue?
592 def overdue?
593 active? && !due_date.nil? && (due_date < Date.today)
593 active? && !due_date.nil? && (due_date < Date.today)
594 end
594 end
595
595
596 # Returns the percent completed for this project, based on the
596 # Returns the percent completed for this project, based on the
597 # progress on it's versions.
597 # progress on it's versions.
598 def completed_percent(options={:include_subprojects => false})
598 def completed_percent(options={:include_subprojects => false})
599 if options.delete(:include_subprojects)
599 if options.delete(:include_subprojects)
600 total = self_and_descendants.collect(&:completed_percent).sum
600 total = self_and_descendants.collect(&:completed_percent).sum
601
601
602 total / self_and_descendants.count
602 total / self_and_descendants.count
603 else
603 else
604 if versions.count > 0
604 if versions.count > 0
605 total = versions.collect(&:completed_percent).sum
605 total = versions.collect(&:completed_percent).sum
606
606
607 total / versions.count
607 total / versions.count
608 else
608 else
609 100
609 100
610 end
610 end
611 end
611 end
612 end
612 end
613
613
614 # Return true if this project allows to do the specified action.
614 # Return true if this project allows to do the specified action.
615 # action can be:
615 # action can be:
616 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
616 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
617 # * a permission Symbol (eg. :edit_project)
617 # * a permission Symbol (eg. :edit_project)
618 def allows_to?(action)
618 def allows_to?(action)
619 if archived?
619 if archived?
620 # No action allowed on archived projects
620 # No action allowed on archived projects
621 return false
621 return false
622 end
622 end
623 unless active? || Redmine::AccessControl.read_action?(action)
623 unless active? || Redmine::AccessControl.read_action?(action)
624 # No write action allowed on closed projects
624 # No write action allowed on closed projects
625 return false
625 return false
626 end
626 end
627 # No action allowed on disabled modules
627 # No action allowed on disabled modules
628 if action.is_a? Hash
628 if action.is_a? Hash
629 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
629 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
630 else
630 else
631 allowed_permissions.include? action
631 allowed_permissions.include? action
632 end
632 end
633 end
633 end
634
634
635 # Return the enabled module with the given name
635 # Return the enabled module with the given name
636 # or nil if the module is not enabled for the project
636 # or nil if the module is not enabled for the project
637 def enabled_module(name)
637 def enabled_module(name)
638 name = name.to_s
638 name = name.to_s
639 enabled_modules.detect {|m| m.name == name}
639 enabled_modules.detect {|m| m.name == name}
640 end
640 end
641
641
642 # Return true if the module with the given name is enabled
642 # Return true if the module with the given name is enabled
643 def module_enabled?(name)
643 def module_enabled?(name)
644 enabled_module(name).present?
644 enabled_module(name).present?
645 end
645 end
646
646
647 def enabled_module_names=(module_names)
647 def enabled_module_names=(module_names)
648 if module_names && module_names.is_a?(Array)
648 if module_names && module_names.is_a?(Array)
649 module_names = module_names.collect(&:to_s).reject(&:blank?)
649 module_names = module_names.collect(&:to_s).reject(&:blank?)
650 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)}
651 else
651 else
652 enabled_modules.clear
652 enabled_modules.clear
653 end
653 end
654 end
654 end
655
655
656 # Returns an array of the enabled modules names
656 # Returns an array of the enabled modules names
657 def enabled_module_names
657 def enabled_module_names
658 enabled_modules.collect(&:name)
658 enabled_modules.collect(&:name)
659 end
659 end
660
660
661 # Enable a specific module
661 # Enable a specific module
662 #
662 #
663 # Examples:
663 # Examples:
664 # project.enable_module!(:issue_tracking)
664 # project.enable_module!(:issue_tracking)
665 # project.enable_module!("issue_tracking")
665 # project.enable_module!("issue_tracking")
666 def enable_module!(name)
666 def enable_module!(name)
667 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)
668 end
668 end
669
669
670 # Disable a module if it exists
670 # Disable a module if it exists
671 #
671 #
672 # Examples:
672 # Examples:
673 # project.disable_module!(:issue_tracking)
673 # project.disable_module!(:issue_tracking)
674 # project.disable_module!("issue_tracking")
674 # project.disable_module!("issue_tracking")
675 # project.disable_module!(project.enabled_modules.first)
675 # project.disable_module!(project.enabled_modules.first)
676 def disable_module!(target)
676 def disable_module!(target)
677 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)
678 target.destroy unless target.blank?
678 target.destroy unless target.blank?
679 end
679 end
680
680
681 safe_attributes 'name',
681 safe_attributes 'name',
682 'description',
682 'description',
683 'homepage',
683 'homepage',
684 'is_public',
684 'is_public',
685 'identifier',
685 'identifier',
686 'custom_field_values',
686 'custom_field_values',
687 'custom_fields',
687 'custom_fields',
688 'tracker_ids',
688 'tracker_ids',
689 'issue_custom_field_ids',
689 'issue_custom_field_ids',
690 'parent_id'
690 'parent_id'
691
691
692 safe_attributes 'enabled_module_names',
692 safe_attributes 'enabled_module_names',
693 :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) }
694
694
695 safe_attributes 'inherit_members',
695 safe_attributes 'inherit_members',
696 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
696 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
697
697
698 def safe_attributes=(attrs, user=User.current)
698 def safe_attributes=(attrs, user=User.current)
699 return unless attrs.is_a?(Hash)
699 return unless attrs.is_a?(Hash)
700 attrs = attrs.deep_dup
700 attrs = attrs.deep_dup
701
701
702 @unallowed_parent_id = nil
702 @unallowed_parent_id = nil
703 parent_id_param = attrs['parent_id'].to_s
703 if new_record? || attrs.key?('parent_id')
704 if parent_id_param.blank? || parent_id_param != parent_id.to_s
704 parent_id_param = attrs['parent_id'].to_s
705 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
705 if new_record? || parent_id_param != parent_id.to_s
706 unless allowed_parents(user).include?(p)
706 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
707 attrs.delete('parent_id')
707 unless allowed_parents(user).include?(p)
708 @unallowed_parent_id = true
708 attrs.delete('parent_id')
709 @unallowed_parent_id = true
710 end
709 end
711 end
710 end
712 end
711
713
712 super(attrs, user)
714 super(attrs, user)
713 end
715 end
714
716
715 # Returns an auto-generated project identifier based on the last identifier used
717 # Returns an auto-generated project identifier based on the last identifier used
716 def self.next_identifier
718 def self.next_identifier
717 p = Project.order('id DESC').first
719 p = Project.order('id DESC').first
718 p.nil? ? nil : p.identifier.to_s.succ
720 p.nil? ? nil : p.identifier.to_s.succ
719 end
721 end
720
722
721 # Copies and saves the Project instance based on the +project+.
723 # Copies and saves the Project instance based on the +project+.
722 # Duplicates the source project's:
724 # Duplicates the source project's:
723 # * Wiki
725 # * Wiki
724 # * Versions
726 # * Versions
725 # * Categories
727 # * Categories
726 # * Issues
728 # * Issues
727 # * Members
729 # * Members
728 # * Queries
730 # * Queries
729 #
731 #
730 # Accepts an +options+ argument to specify what to copy
732 # Accepts an +options+ argument to specify what to copy
731 #
733 #
732 # Examples:
734 # Examples:
733 # project.copy(1) # => copies everything
735 # project.copy(1) # => copies everything
734 # project.copy(1, :only => 'members') # => copies members only
736 # project.copy(1, :only => 'members') # => copies members only
735 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
737 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
736 def copy(project, options={})
738 def copy(project, options={})
737 project = project.is_a?(Project) ? project : Project.find(project)
739 project = project.is_a?(Project) ? project : Project.find(project)
738
740
739 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
741 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
740 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
742 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
741
743
742 Project.transaction do
744 Project.transaction do
743 if save
745 if save
744 reload
746 reload
745 to_be_copied.each do |name|
747 to_be_copied.each do |name|
746 send "copy_#{name}", project
748 send "copy_#{name}", project
747 end
749 end
748 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
750 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
749 save
751 save
750 else
752 else
751 false
753 false
752 end
754 end
753 end
755 end
754 end
756 end
755
757
756 def member_principals
758 def member_principals
757 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
759 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
758 memberships.active
760 memberships.active
759 end
761 end
760
762
761 # Returns a new unsaved Project instance with attributes copied from +project+
763 # Returns a new unsaved Project instance with attributes copied from +project+
762 def self.copy_from(project)
764 def self.copy_from(project)
763 project = project.is_a?(Project) ? project : Project.find(project)
765 project = project.is_a?(Project) ? project : Project.find(project)
764 # clear unique attributes
766 # clear unique attributes
765 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
767 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
766 copy = Project.new(attributes)
768 copy = Project.new(attributes)
767 copy.enabled_module_names = project.enabled_module_names
769 copy.enabled_module_names = project.enabled_module_names
768 copy.trackers = project.trackers
770 copy.trackers = project.trackers
769 copy.custom_values = project.custom_values.collect {|v| v.clone}
771 copy.custom_values = project.custom_values.collect {|v| v.clone}
770 copy.issue_custom_fields = project.issue_custom_fields
772 copy.issue_custom_fields = project.issue_custom_fields
771 copy
773 copy
772 end
774 end
773
775
774 # Yields the given block for each project with its level in the tree
776 # Yields the given block for each project with its level in the tree
775 def self.project_tree(projects, &block)
777 def self.project_tree(projects, &block)
776 ancestors = []
778 ancestors = []
777 projects.sort_by(&:lft).each do |project|
779 projects.sort_by(&:lft).each do |project|
778 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
780 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
779 ancestors.pop
781 ancestors.pop
780 end
782 end
781 yield project, ancestors.size
783 yield project, ancestors.size
782 ancestors << project
784 ancestors << project
783 end
785 end
784 end
786 end
785
787
786 private
788 private
787
789
788 def update_inherited_members
790 def update_inherited_members
789 if parent
791 if parent
790 if inherit_members? && !inherit_members_was
792 if inherit_members? && !inherit_members_was
791 remove_inherited_member_roles
793 remove_inherited_member_roles
792 add_inherited_member_roles
794 add_inherited_member_roles
793 elsif !inherit_members? && inherit_members_was
795 elsif !inherit_members? && inherit_members_was
794 remove_inherited_member_roles
796 remove_inherited_member_roles
795 end
797 end
796 end
798 end
797 end
799 end
798
800
799 def remove_inherited_member_roles
801 def remove_inherited_member_roles
800 member_roles = memberships.map(&:member_roles).flatten
802 member_roles = memberships.map(&:member_roles).flatten
801 member_role_ids = member_roles.map(&:id)
803 member_role_ids = member_roles.map(&:id)
802 member_roles.each do |member_role|
804 member_roles.each do |member_role|
803 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
805 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
804 member_role.destroy
806 member_role.destroy
805 end
807 end
806 end
808 end
807 end
809 end
808
810
809 def add_inherited_member_roles
811 def add_inherited_member_roles
810 if inherit_members? && parent
812 if inherit_members? && parent
811 parent.memberships.each do |parent_member|
813 parent.memberships.each do |parent_member|
812 member = Member.find_or_new(self.id, parent_member.user_id)
814 member = Member.find_or_new(self.id, parent_member.user_id)
813 parent_member.member_roles.each do |parent_member_role|
815 parent_member.member_roles.each do |parent_member_role|
814 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
816 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
815 end
817 end
816 member.save!
818 member.save!
817 end
819 end
818 memberships.reset
820 memberships.reset
819 end
821 end
820 end
822 end
821
823
822 def update_versions_from_hierarchy_change
824 def update_versions_from_hierarchy_change
823 Issue.update_versions_from_hierarchy_change(self)
825 Issue.update_versions_from_hierarchy_change(self)
824 end
826 end
825
827
826 def validate_parent
828 def validate_parent
827 if @unallowed_parent_id
829 if @unallowed_parent_id
828 errors.add(:parent_id, :invalid)
830 errors.add(:parent_id, :invalid)
829 elsif parent_id_changed?
831 elsif parent_id_changed?
830 unless parent.nil? || (parent.active? && move_possible?(parent))
832 unless parent.nil? || (parent.active? && move_possible?(parent))
831 errors.add(:parent_id, :invalid)
833 errors.add(:parent_id, :invalid)
832 end
834 end
833 end
835 end
834 end
836 end
835
837
836 # Copies wiki from +project+
838 # Copies wiki from +project+
837 def copy_wiki(project)
839 def copy_wiki(project)
838 # Check that the source project has a wiki first
840 # Check that the source project has a wiki first
839 unless project.wiki.nil?
841 unless project.wiki.nil?
840 wiki = self.wiki || Wiki.new
842 wiki = self.wiki || Wiki.new
841 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
843 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
842 wiki_pages_map = {}
844 wiki_pages_map = {}
843 project.wiki.pages.each do |page|
845 project.wiki.pages.each do |page|
844 # Skip pages without content
846 # Skip pages without content
845 next if page.content.nil?
847 next if page.content.nil?
846 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
848 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
847 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
849 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
848 new_wiki_page.content = new_wiki_content
850 new_wiki_page.content = new_wiki_content
849 wiki.pages << new_wiki_page
851 wiki.pages << new_wiki_page
850 wiki_pages_map[page.id] = new_wiki_page
852 wiki_pages_map[page.id] = new_wiki_page
851 end
853 end
852
854
853 self.wiki = wiki
855 self.wiki = wiki
854 wiki.save
856 wiki.save
855 # Reproduce page hierarchy
857 # Reproduce page hierarchy
856 project.wiki.pages.each do |page|
858 project.wiki.pages.each do |page|
857 if page.parent_id && wiki_pages_map[page.id]
859 if page.parent_id && wiki_pages_map[page.id]
858 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
860 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
859 wiki_pages_map[page.id].save
861 wiki_pages_map[page.id].save
860 end
862 end
861 end
863 end
862 end
864 end
863 end
865 end
864
866
865 # Copies versions from +project+
867 # Copies versions from +project+
866 def copy_versions(project)
868 def copy_versions(project)
867 project.versions.each do |version|
869 project.versions.each do |version|
868 new_version = Version.new
870 new_version = Version.new
869 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
871 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
870 self.versions << new_version
872 self.versions << new_version
871 end
873 end
872 end
874 end
873
875
874 # Copies issue categories from +project+
876 # Copies issue categories from +project+
875 def copy_issue_categories(project)
877 def copy_issue_categories(project)
876 project.issue_categories.each do |issue_category|
878 project.issue_categories.each do |issue_category|
877 new_issue_category = IssueCategory.new
879 new_issue_category = IssueCategory.new
878 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
880 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
879 self.issue_categories << new_issue_category
881 self.issue_categories << new_issue_category
880 end
882 end
881 end
883 end
882
884
883 # Copies issues from +project+
885 # Copies issues from +project+
884 def copy_issues(project)
886 def copy_issues(project)
885 # Stores the source issue id as a key and the copied issues as the
887 # Stores the source issue id as a key and the copied issues as the
886 # value. Used to map the two together for issue relations.
888 # value. Used to map the two together for issue relations.
887 issues_map = {}
889 issues_map = {}
888
890
889 # Store status and reopen locked/closed versions
891 # Store status and reopen locked/closed versions
890 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
892 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
891 version_statuses.each do |version, status|
893 version_statuses.each do |version, status|
892 version.update_attribute :status, 'open'
894 version.update_attribute :status, 'open'
893 end
895 end
894
896
895 # Get issues sorted by root_id, lft so that parent issues
897 # Get issues sorted by root_id, lft so that parent issues
896 # get copied before their children
898 # get copied before their children
897 project.issues.reorder('root_id, lft').each do |issue|
899 project.issues.reorder('root_id, lft').each do |issue|
898 new_issue = Issue.new
900 new_issue = Issue.new
899 new_issue.copy_from(issue, :subtasks => false, :link => false)
901 new_issue.copy_from(issue, :subtasks => false, :link => false)
900 new_issue.project = self
902 new_issue.project = self
901 # Changing project resets the custom field values
903 # Changing project resets the custom field values
902 # TODO: handle this in Issue#project=
904 # TODO: handle this in Issue#project=
903 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
905 new_issue.custom_field_values = issue.custom_field_values.inject({}) {|h,v| h[v.custom_field_id] = v.value; h}
904 # Reassign fixed_versions by name, since names are unique per project
906 # Reassign fixed_versions by name, since names are unique per project
905 if issue.fixed_version && issue.fixed_version.project == project
907 if issue.fixed_version && issue.fixed_version.project == project
906 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
908 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
907 end
909 end
908 # Reassign version custom field values
910 # Reassign version custom field values
909 new_issue.custom_field_values.each do |custom_value|
911 new_issue.custom_field_values.each do |custom_value|
910 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
912 if custom_value.custom_field.field_format == 'version' && custom_value.value.present?
911 versions = Version.where(:id => custom_value.value).to_a
913 versions = Version.where(:id => custom_value.value).to_a
912 new_value = versions.map do |version|
914 new_value = versions.map do |version|
913 if version.project == project
915 if version.project == project
914 self.versions.detect {|v| v.name == version.name}.try(:id)
916 self.versions.detect {|v| v.name == version.name}.try(:id)
915 else
917 else
916 version.id
918 version.id
917 end
919 end
918 end
920 end
919 new_value.compact!
921 new_value.compact!
920 new_value = new_value.first unless custom_value.custom_field.multiple?
922 new_value = new_value.first unless custom_value.custom_field.multiple?
921 custom_value.value = new_value
923 custom_value.value = new_value
922 end
924 end
923 end
925 end
924 # Reassign the category by name, since names are unique per project
926 # Reassign the category by name, since names are unique per project
925 if issue.category
927 if issue.category
926 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
928 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
927 end
929 end
928 # Parent issue
930 # Parent issue
929 if issue.parent_id
931 if issue.parent_id
930 if copied_parent = issues_map[issue.parent_id]
932 if copied_parent = issues_map[issue.parent_id]
931 new_issue.parent_issue_id = copied_parent.id
933 new_issue.parent_issue_id = copied_parent.id
932 end
934 end
933 end
935 end
934
936
935 self.issues << new_issue
937 self.issues << new_issue
936 if new_issue.new_record?
938 if new_issue.new_record?
937 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
939 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
938 else
940 else
939 issues_map[issue.id] = new_issue unless new_issue.new_record?
941 issues_map[issue.id] = new_issue unless new_issue.new_record?
940 end
942 end
941 end
943 end
942
944
943 # Restore locked/closed version statuses
945 # Restore locked/closed version statuses
944 version_statuses.each do |version, status|
946 version_statuses.each do |version, status|
945 version.update_attribute :status, status
947 version.update_attribute :status, status
946 end
948 end
947
949
948 # Relations after in case issues related each other
950 # Relations after in case issues related each other
949 project.issues.each do |issue|
951 project.issues.each do |issue|
950 new_issue = issues_map[issue.id]
952 new_issue = issues_map[issue.id]
951 unless new_issue
953 unless new_issue
952 # Issue was not copied
954 # Issue was not copied
953 next
955 next
954 end
956 end
955
957
956 # Relations
958 # Relations
957 issue.relations_from.each do |source_relation|
959 issue.relations_from.each do |source_relation|
958 new_issue_relation = IssueRelation.new
960 new_issue_relation = IssueRelation.new
959 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
961 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
960 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
962 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
961 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
963 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
962 new_issue_relation.issue_to = source_relation.issue_to
964 new_issue_relation.issue_to = source_relation.issue_to
963 end
965 end
964 new_issue.relations_from << new_issue_relation
966 new_issue.relations_from << new_issue_relation
965 end
967 end
966
968
967 issue.relations_to.each do |source_relation|
969 issue.relations_to.each do |source_relation|
968 new_issue_relation = IssueRelation.new
970 new_issue_relation = IssueRelation.new
969 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
971 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
970 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
972 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
971 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
973 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
972 new_issue_relation.issue_from = source_relation.issue_from
974 new_issue_relation.issue_from = source_relation.issue_from
973 end
975 end
974 new_issue.relations_to << new_issue_relation
976 new_issue.relations_to << new_issue_relation
975 end
977 end
976 end
978 end
977 end
979 end
978
980
979 # Copies members from +project+
981 # Copies members from +project+
980 def copy_members(project)
982 def copy_members(project)
981 # Copy users first, then groups to handle members with inherited and given roles
983 # Copy users first, then groups to handle members with inherited and given roles
982 members_to_copy = []
984 members_to_copy = []
983 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
985 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)}
986 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
985
987
986 members_to_copy.each do |member|
988 members_to_copy.each do |member|
987 new_member = Member.new
989 new_member = Member.new
988 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
990 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
989 # only copy non inherited roles
991 # only copy non inherited roles
990 # inherited roles will be added when copying the group membership
992 # inherited roles will be added when copying the group membership
991 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
993 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
992 next if role_ids.empty?
994 next if role_ids.empty?
993 new_member.role_ids = role_ids
995 new_member.role_ids = role_ids
994 new_member.project = self
996 new_member.project = self
995 self.members << new_member
997 self.members << new_member
996 end
998 end
997 end
999 end
998
1000
999 # Copies queries from +project+
1001 # Copies queries from +project+
1000 def copy_queries(project)
1002 def copy_queries(project)
1001 project.queries.each do |query|
1003 project.queries.each do |query|
1002 new_query = IssueQuery.new
1004 new_query = IssueQuery.new
1003 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1005 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
1004 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1006 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
1005 new_query.project = self
1007 new_query.project = self
1006 new_query.user_id = query.user_id
1008 new_query.user_id = query.user_id
1007 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1009 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
1008 self.queries << new_query
1010 self.queries << new_query
1009 end
1011 end
1010 end
1012 end
1011
1013
1012 # Copies boards from +project+
1014 # Copies boards from +project+
1013 def copy_boards(project)
1015 def copy_boards(project)
1014 project.boards.each do |board|
1016 project.boards.each do |board|
1015 new_board = Board.new
1017 new_board = Board.new
1016 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1018 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
1017 new_board.project = self
1019 new_board.project = self
1018 self.boards << new_board
1020 self.boards << new_board
1019 end
1021 end
1020 end
1022 end
1021
1023
1022 def allowed_permissions
1024 def allowed_permissions
1023 @allowed_permissions ||= begin
1025 @allowed_permissions ||= begin
1024 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1026 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
1025 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1027 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
1026 end
1028 end
1027 end
1029 end
1028
1030
1029 def allowed_actions
1031 def allowed_actions
1030 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1032 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1031 end
1033 end
1032
1034
1033 # Archives subprojects recursively
1035 # Archives subprojects recursively
1034 def archive!
1036 def archive!
1035 children.each do |subproject|
1037 children.each do |subproject|
1036 subproject.send :archive!
1038 subproject.send :archive!
1037 end
1039 end
1038 update_attribute :status, STATUS_ARCHIVED
1040 update_attribute :status, STATUS_ARCHIVED
1039 end
1041 end
1040 end
1042 end
@@ -1,707 +1,718
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_update_child_project_without_parent_permission_should_not_show_validation_error
499 child = Project.generate_with_parent!
500 user = User.generate!
501 User.add_to_project(user, child, Role.generate!(:permissions => [:edit_project]))
502 @request.session[:user_id] = user.id
503
504 post :update, :id => child.id, :project => {:name => 'Updated'}
505 assert_response 302
506 assert_match /Successful update/, flash[:notice]
507 end
508
498 def test_modules
509 def test_modules
499 @request.session[:user_id] = 2
510 @request.session[:user_id] = 2
500 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
511 Project.find(1).enabled_module_names = ['issue_tracking', 'news']
501
512
502 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
513 post :modules, :id => 1, :enabled_module_names => ['issue_tracking', 'repository', 'documents']
503 assert_redirected_to '/projects/ecookbook/settings/modules'
514 assert_redirected_to '/projects/ecookbook/settings/modules'
504 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
515 assert_equal ['documents', 'issue_tracking', 'repository'], Project.find(1).enabled_module_names.sort
505 end
516 end
506
517
507 def test_destroy_leaf_project_without_confirmation_should_show_confirmation
518 def test_destroy_leaf_project_without_confirmation_should_show_confirmation
508 @request.session[:user_id] = 1 # admin
519 @request.session[:user_id] = 1 # admin
509
520
510 assert_no_difference 'Project.count' do
521 assert_no_difference 'Project.count' do
511 delete :destroy, :id => 2
522 delete :destroy, :id => 2
512 assert_response :success
523 assert_response :success
513 assert_template 'destroy'
524 assert_template 'destroy'
514 end
525 end
515 end
526 end
516
527
517 def test_destroy_without_confirmation_should_show_confirmation_with_subprojects
528 def test_destroy_without_confirmation_should_show_confirmation_with_subprojects
518 @request.session[:user_id] = 1 # admin
529 @request.session[:user_id] = 1 # admin
519
530
520 assert_no_difference 'Project.count' do
531 assert_no_difference 'Project.count' do
521 delete :destroy, :id => 1
532 delete :destroy, :id => 1
522 assert_response :success
533 assert_response :success
523 assert_template 'destroy'
534 assert_template 'destroy'
524 end
535 end
525 assert_select 'strong',
536 assert_select 'strong',
526 :text => ['Private child of eCookbook',
537 :text => ['Private child of eCookbook',
527 'Child of private child, eCookbook Subproject 1',
538 'Child of private child, eCookbook Subproject 1',
528 'eCookbook Subproject 2'].join(', ')
539 'eCookbook Subproject 2'].join(', ')
529 end
540 end
530
541
531 def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects
542 def test_destroy_with_confirmation_should_destroy_the_project_and_subprojects
532 @request.session[:user_id] = 1 # admin
543 @request.session[:user_id] = 1 # admin
533
544
534 assert_difference 'Project.count', -5 do
545 assert_difference 'Project.count', -5 do
535 delete :destroy, :id => 1, :confirm => 1
546 delete :destroy, :id => 1, :confirm => 1
536 assert_redirected_to '/admin/projects'
547 assert_redirected_to '/admin/projects'
537 end
548 end
538 assert_nil Project.find_by_id(1)
549 assert_nil Project.find_by_id(1)
539 end
550 end
540
551
541 def test_archive
552 def test_archive
542 @request.session[:user_id] = 1 # admin
553 @request.session[:user_id] = 1 # admin
543 post :archive, :id => 1
554 post :archive, :id => 1
544 assert_redirected_to '/admin/projects'
555 assert_redirected_to '/admin/projects'
545 assert !Project.find(1).active?
556 assert !Project.find(1).active?
546 end
557 end
547
558
548 def test_archive_with_failure
559 def test_archive_with_failure
549 @request.session[:user_id] = 1
560 @request.session[:user_id] = 1
550 Project.any_instance.stubs(:archive).returns(false)
561 Project.any_instance.stubs(:archive).returns(false)
551 post :archive, :id => 1
562 post :archive, :id => 1
552 assert_redirected_to '/admin/projects'
563 assert_redirected_to '/admin/projects'
553 assert_match /project cannot be archived/i, flash[:error]
564 assert_match /project cannot be archived/i, flash[:error]
554 end
565 end
555
566
556 def test_unarchive
567 def test_unarchive
557 @request.session[:user_id] = 1 # admin
568 @request.session[:user_id] = 1 # admin
558 Project.find(1).archive
569 Project.find(1).archive
559 post :unarchive, :id => 1
570 post :unarchive, :id => 1
560 assert_redirected_to '/admin/projects'
571 assert_redirected_to '/admin/projects'
561 assert Project.find(1).active?
572 assert Project.find(1).active?
562 end
573 end
563
574
564 def test_close
575 def test_close
565 @request.session[:user_id] = 2
576 @request.session[:user_id] = 2
566 post :close, :id => 1
577 post :close, :id => 1
567 assert_redirected_to '/projects/ecookbook'
578 assert_redirected_to '/projects/ecookbook'
568 assert_equal Project::STATUS_CLOSED, Project.find(1).status
579 assert_equal Project::STATUS_CLOSED, Project.find(1).status
569 end
580 end
570
581
571 def test_reopen
582 def test_reopen
572 Project.find(1).close
583 Project.find(1).close
573 @request.session[:user_id] = 2
584 @request.session[:user_id] = 2
574 post :reopen, :id => 1
585 post :reopen, :id => 1
575 assert_redirected_to '/projects/ecookbook'
586 assert_redirected_to '/projects/ecookbook'
576 assert Project.find(1).active?
587 assert Project.find(1).active?
577 end
588 end
578
589
579 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
590 def test_project_breadcrumbs_should_be_limited_to_3_ancestors
580 CustomField.delete_all
591 CustomField.delete_all
581 parent = nil
592 parent = nil
582 6.times do |i|
593 6.times do |i|
583 p = Project.generate_with_parent!(parent)
594 p = Project.generate_with_parent!(parent)
584 get :show, :id => p
595 get :show, :id => p
585 assert_select '#header h1' do
596 assert_select '#header h1' do
586 assert_select 'a', :count => [i, 3].min
597 assert_select 'a', :count => [i, 3].min
587 end
598 end
588
599
589 parent = p
600 parent = p
590 end
601 end
591 end
602 end
592
603
593 def test_get_copy
604 def test_get_copy
594 @request.session[:user_id] = 1 # admin
605 @request.session[:user_id] = 1 # admin
595 get :copy, :id => 1
606 get :copy, :id => 1
596 assert_response :success
607 assert_response :success
597 assert_template 'copy'
608 assert_template 'copy'
598 assert assigns(:project)
609 assert assigns(:project)
599 assert_equal Project.find(1).description, assigns(:project).description
610 assert_equal Project.find(1).description, assigns(:project).description
600 assert_nil assigns(:project).id
611 assert_nil assigns(:project).id
601
612
602 assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1
613 assert_select 'input[name=?][value=?]', 'project[enabled_module_names][]', 'issue_tracking', 1
603 end
614 end
604
615
605 def test_get_copy_with_invalid_source_should_respond_with_404
616 def test_get_copy_with_invalid_source_should_respond_with_404
606 @request.session[:user_id] = 1
617 @request.session[:user_id] = 1
607 get :copy, :id => 99
618 get :copy, :id => 99
608 assert_response 404
619 assert_response 404
609 end
620 end
610
621
611 def test_get_copy_should_preselect_custom_fields
622 def test_get_copy_should_preselect_custom_fields
612 field1 = IssueCustomField.generate!(:is_for_all => false)
623 field1 = IssueCustomField.generate!(:is_for_all => false)
613 field2 = IssueCustomField.generate!(:is_for_all => false)
624 field2 = IssueCustomField.generate!(:is_for_all => false)
614 source = Project.generate!(:issue_custom_fields => [field1])
625 source = Project.generate!(:issue_custom_fields => [field1])
615 @request.session[:user_id] = 1
626 @request.session[:user_id] = 1
616
627
617 get :copy, :id => source.id
628 get :copy, :id => source.id
618 assert_response :success
629 assert_response :success
619 assert_select 'fieldset#project_issue_custom_fields' do
630 assert_select 'fieldset#project_issue_custom_fields' do
620 assert_select 'input[type=checkbox][value=?][checked=checked]', field1.id.to_s
631 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
632 assert_select 'input[type=checkbox][value=?]:not([checked])', field2.id.to_s
622 end
633 end
623 end
634 end
624
635
625 def test_post_copy_should_copy_requested_items
636 def test_post_copy_should_copy_requested_items
626 @request.session[:user_id] = 1 # admin
637 @request.session[:user_id] = 1 # admin
627 CustomField.delete_all
638 CustomField.delete_all
628
639
629 assert_difference 'Project.count' do
640 assert_difference 'Project.count' do
630 post :copy, :id => 1,
641 post :copy, :id => 1,
631 :project => {
642 :project => {
632 :name => 'Copy',
643 :name => 'Copy',
633 :identifier => 'unique-copy',
644 :identifier => 'unique-copy',
634 :tracker_ids => ['1', '2', '3', ''],
645 :tracker_ids => ['1', '2', '3', ''],
635 :enabled_module_names => %w(issue_tracking time_tracking)
646 :enabled_module_names => %w(issue_tracking time_tracking)
636 },
647 },
637 :only => %w(issues versions)
648 :only => %w(issues versions)
638 end
649 end
639 project = Project.find('unique-copy')
650 project = Project.find('unique-copy')
640 source = Project.find(1)
651 source = Project.find(1)
641 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
652 assert_equal %w(issue_tracking time_tracking), project.enabled_module_names.sort
642
653
643 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
654 assert_equal source.versions.count, project.versions.count, "All versions were not copied"
644 assert_equal source.issues.count, project.issues.count, "All issues were not copied"
655 assert_equal source.issues.count, project.issues.count, "All issues were not copied"
645 assert_equal 0, project.members.count
656 assert_equal 0, project.members.count
646 end
657 end
647
658
648 def test_post_copy_should_redirect_to_settings_when_successful
659 def test_post_copy_should_redirect_to_settings_when_successful
649 @request.session[:user_id] = 1 # admin
660 @request.session[:user_id] = 1 # admin
650 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
661 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => 'unique-copy'}
651 assert_response :redirect
662 assert_response :redirect
652 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
663 assert_redirected_to :controller => 'projects', :action => 'settings', :id => 'unique-copy'
653 end
664 end
654
665
655 def test_post_copy_with_failure
666 def test_post_copy_with_failure
656 @request.session[:user_id] = 1
667 @request.session[:user_id] = 1
657 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => ''}
668 post :copy, :id => 1, :project => {:name => 'Copy', :identifier => ''}
658 assert_response :success
669 assert_response :success
659 assert_template 'copy'
670 assert_template 'copy'
660 end
671 end
661
672
662 def test_jump_should_redirect_to_active_tab
673 def test_jump_should_redirect_to_active_tab
663 get :show, :id => 1, :jump => 'issues'
674 get :show, :id => 1, :jump => 'issues'
664 assert_redirected_to '/projects/ecookbook/issues'
675 assert_redirected_to '/projects/ecookbook/issues'
665 end
676 end
666
677
667 def test_jump_should_not_redirect_to_inactive_tab
678 def test_jump_should_not_redirect_to_inactive_tab
668 get :show, :id => 3, :jump => 'documents'
679 get :show, :id => 3, :jump => 'documents'
669 assert_response :success
680 assert_response :success
670 assert_template 'show'
681 assert_template 'show'
671 end
682 end
672
683
673 def test_jump_should_not_redirect_to_unknown_tab
684 def test_jump_should_not_redirect_to_unknown_tab
674 get :show, :id => 3, :jump => 'foobar'
685 get :show, :id => 3, :jump => 'foobar'
675 assert_response :success
686 assert_response :success
676 assert_template 'show'
687 assert_template 'show'
677 end
688 end
678
689
679 def test_body_should_have_project_css_class
690 def test_body_should_have_project_css_class
680 get :show, :id => 1
691 get :show, :id => 1
681 assert_select 'body.project-ecookbook'
692 assert_select 'body.project-ecookbook'
682 end
693 end
683
694
684 def test_project_menu_should_include_new_issue_link
695 def test_project_menu_should_include_new_issue_link
685 @request.session[:user_id] = 2
696 @request.session[:user_id] = 2
686 get :show, :id => 1
697 get :show, :id => 1
687 assert_select '#main-menu a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue'
698 assert_select '#main-menu a.new-issue[href="/projects/ecookbook/issues/new"]', :text => 'New issue'
688 end
699 end
689
700
690 def test_project_menu_should_not_include_new_issue_link_for_project_without_trackers
701 def test_project_menu_should_not_include_new_issue_link_for_project_without_trackers
691 Project.find(1).trackers.clear
702 Project.find(1).trackers.clear
692
703
693 @request.session[:user_id] = 2
704 @request.session[:user_id] = 2
694 get :show, :id => 1
705 get :show, :id => 1
695 assert_select '#main-menu a.new-issue', 0
706 assert_select '#main-menu a.new-issue', 0
696 end
707 end
697
708
698 def test_project_menu_should_not_include_new_issue_link_for_users_with_copy_issues_permission_only
709 def test_project_menu_should_not_include_new_issue_link_for_users_with_copy_issues_permission_only
699 role = Role.find(1)
710 role = Role.find(1)
700 role.remove_permission! :add_issues
711 role.remove_permission! :add_issues
701 role.add_permission! :copy_issues
712 role.add_permission! :copy_issues
702
713
703 @request.session[:user_id] = 2
714 @request.session[:user_id] = 2
704 get :show, :id => 1
715 get :show, :id => 1
705 assert_select '#main-menu a.new-issue', 0
716 assert_select '#main-menu a.new-issue', 0
706 end
717 end
707 end
718 end
@@ -1,261 +1,264
1 module ObjectHelpers
1 module ObjectHelpers
2 def User.generate!(attributes={})
2 def User.generate!(attributes={})
3 @generated_user_login ||= 'user0'
3 @generated_user_login ||= 'user0'
4 @generated_user_login.succ!
4 @generated_user_login.succ!
5 user = User.new(attributes)
5 user = User.new(attributes)
6 user.login = @generated_user_login.dup if user.login.blank?
6 user.login = @generated_user_login.dup if user.login.blank?
7 user.mail = "#{@generated_user_login}@example.com" if user.mail.blank?
7 user.mail = "#{@generated_user_login}@example.com" if user.mail.blank?
8 user.firstname = "Bob" if user.firstname.blank?
8 user.firstname = "Bob" if user.firstname.blank?
9 user.lastname = "Doe" if user.lastname.blank?
9 user.lastname = "Doe" if user.lastname.blank?
10 yield user if block_given?
10 yield user if block_given?
11 user.save!
11 user.save!
12 user
12 user
13 end
13 end
14
14
15 def User.add_to_project(user, project, roles=nil)
15 def User.add_to_project(user, project, roles=nil)
16 roles = Role.find(1) if roles.nil?
16 roles = Role.find(1) if roles.nil?
17 roles = [roles] if roles.is_a?(Role)
17 roles = [roles] if roles.is_a?(Role)
18 Member.create!(:principal => user, :project => project, :roles => roles)
18 Member.create!(:principal => user, :project => project, :roles => roles)
19 end
19 end
20
20
21 def Group.generate!(attributes={})
21 def Group.generate!(attributes={})
22 @generated_group_name ||= 'Group 0'
22 @generated_group_name ||= 'Group 0'
23 @generated_group_name.succ!
23 @generated_group_name.succ!
24 group = Group.new(attributes)
24 group = Group.new(attributes)
25 group.name = @generated_group_name.dup if group.name.blank?
25 group.name = @generated_group_name.dup if group.name.blank?
26 yield group if block_given?
26 yield group if block_given?
27 group.save!
27 group.save!
28 group
28 group
29 end
29 end
30
30
31 def Project.generate!(attributes={})
31 def Project.generate!(attributes={})
32 @generated_project_identifier ||= 'project-0000'
32 @generated_project_identifier ||= 'project-0000'
33 @generated_project_identifier.succ!
33 @generated_project_identifier.succ!
34 project = Project.new(attributes)
34 project = Project.new(attributes)
35 project.name = @generated_project_identifier.dup if project.name.blank?
35 project.name = @generated_project_identifier.dup if project.name.blank?
36 project.identifier = @generated_project_identifier.dup if project.identifier.blank?
36 project.identifier = @generated_project_identifier.dup if project.identifier.blank?
37 yield project if block_given?
37 yield project if block_given?
38 project.save!
38 project.save!
39 project
39 project
40 end
40 end
41
41
42 def Project.generate_with_parent!(parent, attributes={})
42 def Project.generate_with_parent!(*args)
43 attributes = args.last.is_a?(Hash) ? args.pop : {}
44 parent = args.size > 0 ? args.first : Project.generate!
45
43 project = Project.generate!(attributes) do |p|
46 project = Project.generate!(attributes) do |p|
44 p.parent = parent
47 p.parent = parent
45 end
48 end
46 parent.reload if parent
49 parent.reload if parent
47 project
50 project
48 end
51 end
49
52
50 def IssueStatus.generate!(attributes={})
53 def IssueStatus.generate!(attributes={})
51 @generated_status_name ||= 'Status 0'
54 @generated_status_name ||= 'Status 0'
52 @generated_status_name.succ!
55 @generated_status_name.succ!
53 status = IssueStatus.new(attributes)
56 status = IssueStatus.new(attributes)
54 status.name = @generated_status_name.dup if status.name.blank?
57 status.name = @generated_status_name.dup if status.name.blank?
55 yield status if block_given?
58 yield status if block_given?
56 status.save!
59 status.save!
57 status
60 status
58 end
61 end
59
62
60 def Tracker.generate!(attributes={})
63 def Tracker.generate!(attributes={})
61 @generated_tracker_name ||= 'Tracker 0'
64 @generated_tracker_name ||= 'Tracker 0'
62 @generated_tracker_name.succ!
65 @generated_tracker_name.succ!
63 tracker = Tracker.new(attributes)
66 tracker = Tracker.new(attributes)
64 tracker.name = @generated_tracker_name.dup if tracker.name.blank?
67 tracker.name = @generated_tracker_name.dup if tracker.name.blank?
65 tracker.default_status ||= IssueStatus.order('position').first || IssueStatus.generate!
68 tracker.default_status ||= IssueStatus.order('position').first || IssueStatus.generate!
66 yield tracker if block_given?
69 yield tracker if block_given?
67 tracker.save!
70 tracker.save!
68 tracker
71 tracker
69 end
72 end
70
73
71 def Role.generate!(attributes={})
74 def Role.generate!(attributes={})
72 @generated_role_name ||= 'Role 0'
75 @generated_role_name ||= 'Role 0'
73 @generated_role_name.succ!
76 @generated_role_name.succ!
74 role = Role.new(attributes)
77 role = Role.new(attributes)
75 role.name = @generated_role_name.dup if role.name.blank?
78 role.name = @generated_role_name.dup if role.name.blank?
76 yield role if block_given?
79 yield role if block_given?
77 role.save!
80 role.save!
78 role
81 role
79 end
82 end
80
83
81 # Generates an unsaved Issue
84 # Generates an unsaved Issue
82 def Issue.generate(attributes={})
85 def Issue.generate(attributes={})
83 issue = Issue.new(attributes)
86 issue = Issue.new(attributes)
84 issue.project ||= Project.find(1)
87 issue.project ||= Project.find(1)
85 issue.tracker ||= issue.project.trackers.first
88 issue.tracker ||= issue.project.trackers.first
86 issue.subject = 'Generated' if issue.subject.blank?
89 issue.subject = 'Generated' if issue.subject.blank?
87 issue.author ||= User.find(2)
90 issue.author ||= User.find(2)
88 yield issue if block_given?
91 yield issue if block_given?
89 issue
92 issue
90 end
93 end
91
94
92 # Generates a saved Issue
95 # Generates a saved Issue
93 def Issue.generate!(attributes={}, &block)
96 def Issue.generate!(attributes={}, &block)
94 issue = Issue.generate(attributes, &block)
97 issue = Issue.generate(attributes, &block)
95 issue.save!
98 issue.save!
96 issue
99 issue
97 end
100 end
98
101
99 # Generates an issue with 2 children and a grandchild
102 # Generates an issue with 2 children and a grandchild
100 def Issue.generate_with_descendants!(attributes={})
103 def Issue.generate_with_descendants!(attributes={})
101 issue = Issue.generate!(attributes)
104 issue = Issue.generate!(attributes)
102 child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id)
105 child = Issue.generate!(:project => issue.project, :subject => 'Child1', :parent_issue_id => issue.id)
103 Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id)
106 Issue.generate!(:project => issue.project, :subject => 'Child2', :parent_issue_id => issue.id)
104 Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id)
107 Issue.generate!(:project => issue.project, :subject => 'Child11', :parent_issue_id => child.id)
105 issue.reload
108 issue.reload
106 end
109 end
107
110
108 def Issue.generate_with_child!(attributes={})
111 def Issue.generate_with_child!(attributes={})
109 issue = Issue.generate!(attributes)
112 issue = Issue.generate!(attributes)
110 Issue.generate!(:parent_issue_id => issue.id)
113 Issue.generate!(:parent_issue_id => issue.id)
111 issue.reload
114 issue.reload
112 end
115 end
113
116
114 def Journal.generate!(attributes={})
117 def Journal.generate!(attributes={})
115 journal = Journal.new(attributes)
118 journal = Journal.new(attributes)
116 journal.user ||= User.first
119 journal.user ||= User.first
117 journal.journalized ||= Issue.first
120 journal.journalized ||= Issue.first
118 yield journal if block_given?
121 yield journal if block_given?
119 journal.save!
122 journal.save!
120 journal
123 journal
121 end
124 end
122
125
123 def Version.generate!(attributes={})
126 def Version.generate!(attributes={})
124 @generated_version_name ||= 'Version 0'
127 @generated_version_name ||= 'Version 0'
125 @generated_version_name.succ!
128 @generated_version_name.succ!
126 version = Version.new(attributes)
129 version = Version.new(attributes)
127 version.name = @generated_version_name.dup if version.name.blank?
130 version.name = @generated_version_name.dup if version.name.blank?
128 version.project ||= Project.find(1)
131 version.project ||= Project.find(1)
129 yield version if block_given?
132 yield version if block_given?
130 version.save!
133 version.save!
131 version
134 version
132 end
135 end
133
136
134 def TimeEntry.generate!(attributes={})
137 def TimeEntry.generate!(attributes={})
135 entry = TimeEntry.new(attributes)
138 entry = TimeEntry.new(attributes)
136 entry.user ||= User.find(2)
139 entry.user ||= User.find(2)
137 entry.issue ||= Issue.find(1) unless entry.project
140 entry.issue ||= Issue.find(1) unless entry.project
138 entry.project ||= entry.issue.project
141 entry.project ||= entry.issue.project
139 entry.activity ||= TimeEntryActivity.first
142 entry.activity ||= TimeEntryActivity.first
140 entry.spent_on ||= Date.today
143 entry.spent_on ||= Date.today
141 entry.hours ||= 1.0
144 entry.hours ||= 1.0
142 entry.save!
145 entry.save!
143 entry
146 entry
144 end
147 end
145
148
146 def AuthSource.generate!(attributes={})
149 def AuthSource.generate!(attributes={})
147 @generated_auth_source_name ||= 'Auth 0'
150 @generated_auth_source_name ||= 'Auth 0'
148 @generated_auth_source_name.succ!
151 @generated_auth_source_name.succ!
149 source = AuthSource.new(attributes)
152 source = AuthSource.new(attributes)
150 source.name = @generated_auth_source_name.dup if source.name.blank?
153 source.name = @generated_auth_source_name.dup if source.name.blank?
151 yield source if block_given?
154 yield source if block_given?
152 source.save!
155 source.save!
153 source
156 source
154 end
157 end
155
158
156 def Board.generate!(attributes={})
159 def Board.generate!(attributes={})
157 @generated_board_name ||= 'Forum 0'
160 @generated_board_name ||= 'Forum 0'
158 @generated_board_name.succ!
161 @generated_board_name.succ!
159 board = Board.new(attributes)
162 board = Board.new(attributes)
160 board.name = @generated_board_name.dup if board.name.blank?
163 board.name = @generated_board_name.dup if board.name.blank?
161 board.description = @generated_board_name.dup if board.description.blank?
164 board.description = @generated_board_name.dup if board.description.blank?
162 yield board if block_given?
165 yield board if block_given?
163 board.save!
166 board.save!
164 board
167 board
165 end
168 end
166
169
167 def Attachment.generate!(attributes={})
170 def Attachment.generate!(attributes={})
168 @generated_filename ||= 'testfile0'
171 @generated_filename ||= 'testfile0'
169 @generated_filename.succ!
172 @generated_filename.succ!
170 attributes = attributes.dup
173 attributes = attributes.dup
171 attachment = Attachment.new(attributes)
174 attachment = Attachment.new(attributes)
172 attachment.container ||= Issue.find(1)
175 attachment.container ||= Issue.find(1)
173 attachment.author ||= User.find(2)
176 attachment.author ||= User.find(2)
174 attachment.filename = @generated_filename.dup if attachment.filename.blank?
177 attachment.filename = @generated_filename.dup if attachment.filename.blank?
175 attachment.save!
178 attachment.save!
176 attachment
179 attachment
177 end
180 end
178
181
179 def CustomField.generate!(attributes={})
182 def CustomField.generate!(attributes={})
180 @generated_custom_field_name ||= 'Custom field 0'
183 @generated_custom_field_name ||= 'Custom field 0'
181 @generated_custom_field_name.succ!
184 @generated_custom_field_name.succ!
182 field = new(attributes)
185 field = new(attributes)
183 field.name = @generated_custom_field_name.dup if field.name.blank?
186 field.name = @generated_custom_field_name.dup if field.name.blank?
184 field.field_format = 'string' if field.field_format.blank?
187 field.field_format = 'string' if field.field_format.blank?
185 yield field if block_given?
188 yield field if block_given?
186 field.save!
189 field.save!
187 field
190 field
188 end
191 end
189
192
190 def Changeset.generate!(attributes={})
193 def Changeset.generate!(attributes={})
191 @generated_changeset_rev ||= '123456'
194 @generated_changeset_rev ||= '123456'
192 @generated_changeset_rev.succ!
195 @generated_changeset_rev.succ!
193 changeset = new(attributes)
196 changeset = new(attributes)
194 changeset.repository ||= Project.find(1).repository
197 changeset.repository ||= Project.find(1).repository
195 changeset.revision ||= @generated_changeset_rev
198 changeset.revision ||= @generated_changeset_rev
196 changeset.committed_on ||= Time.now
199 changeset.committed_on ||= Time.now
197 yield changeset if block_given?
200 yield changeset if block_given?
198 changeset.save!
201 changeset.save!
199 changeset
202 changeset
200 end
203 end
201
204
202 def Query.generate!(attributes={})
205 def Query.generate!(attributes={})
203 query = new(attributes)
206 query = new(attributes)
204 query.name = "Generated query" if query.name.blank?
207 query.name = "Generated query" if query.name.blank?
205 query.user ||= User.find(1)
208 query.user ||= User.find(1)
206 query.save!
209 query.save!
207 query
210 query
208 end
211 end
209
212
210 def generate_import(fixture_name='import_issues.csv')
213 def generate_import(fixture_name='import_issues.csv')
211 import = IssueImport.new
214 import = IssueImport.new
212 import.user_id = 2
215 import.user_id = 2
213 import.file = uploaded_test_file(fixture_name, 'text/csv')
216 import.file = uploaded_test_file(fixture_name, 'text/csv')
214 import.save!
217 import.save!
215 import
218 import
216 end
219 end
217
220
218 def generate_import_with_mapping(fixture_name='import_issues.csv')
221 def generate_import_with_mapping(fixture_name='import_issues.csv')
219 import = generate_import(fixture_name)
222 import = generate_import(fixture_name)
220
223
221 import.settings = {
224 import.settings = {
222 'separator' => ";", 'wrapper' => '"', 'encoding' => "UTF-8",
225 'separator' => ";", 'wrapper' => '"', 'encoding' => "UTF-8",
223 'mapping' => {'project_id' => '1', 'tracker_id' => '2', 'subject' => '1'}
226 'mapping' => {'project_id' => '1', 'tracker_id' => '2', 'subject' => '1'}
224 }
227 }
225 import.save!
228 import.save!
226 import
229 import
227 end
230 end
228 end
231 end
229
232
230 module TrackerObjectHelpers
233 module TrackerObjectHelpers
231 def generate_transitions!(*args)
234 def generate_transitions!(*args)
232 options = args.last.is_a?(Hash) ? args.pop : {}
235 options = args.last.is_a?(Hash) ? args.pop : {}
233 if args.size == 1
236 if args.size == 1
234 args << args.first
237 args << args.first
235 end
238 end
236 if options[:clear]
239 if options[:clear]
237 WorkflowTransition.where(:tracker_id => id).delete_all
240 WorkflowTransition.where(:tracker_id => id).delete_all
238 end
241 end
239 args.each_cons(2) do |old_status_id, new_status_id|
242 args.each_cons(2) do |old_status_id, new_status_id|
240 WorkflowTransition.create!(
243 WorkflowTransition.create!(
241 :tracker => self,
244 :tracker => self,
242 :role_id => (options[:role_id] || 1),
245 :role_id => (options[:role_id] || 1),
243 :old_status_id => old_status_id,
246 :old_status_id => old_status_id,
244 :new_status_id => new_status_id
247 :new_status_id => new_status_id
245 )
248 )
246 end
249 end
247 end
250 end
248 end
251 end
249 Tracker.send :include, TrackerObjectHelpers
252 Tracker.send :include, TrackerObjectHelpers
250
253
251 module IssueObjectHelpers
254 module IssueObjectHelpers
252 def close!
255 def close!
253 self.status = IssueStatus.where(:is_closed => true).first
256 self.status = IssueStatus.where(:is_closed => true).first
254 save!
257 save!
255 end
258 end
256
259
257 def generate_child!(attributes={})
260 def generate_child!(attributes={})
258 Issue.generate!(attributes.merge(:parent_issue_id => self.id))
261 Issue.generate!(attributes.merge(:parent_issue_id => self.id))
259 end
262 end
260 end
263 end
261 Issue.send :include, IssueObjectHelpers
264 Issue.send :include, IssueObjectHelpers
General Comments 0
You need to be logged in to leave comments. Login now