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