##// END OF EJS Templates
Merged r14508 (#20512)....
Jean-Philippe Lang -
r14174:637ec7e88364
parent child
Show More
@@ -1,1011 +1,1011
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 parent_id_param = attrs['parent_id'].to_s
691 if parent_id_param.blank? || parent_id_param != parent_id.to_s
691 if parent_id_param.blank? || parent_id_param != parent_id.to_s
692 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
692 p = parent_id_param.present? ? Project.find_by_id(parent_id_param) : nil
693 unless allowed_parents(user).include?(p)
693 unless allowed_parents(user).include?(p)
694 attrs.delete('parent_id')
694 attrs.delete('parent_id')
695 @unallowed_parent_id = true
695 @unallowed_parent_id = true
696 end
696 end
697 end
697 end
698
698
699 super(attrs, user)
699 super(attrs, user)
700 end
700 end
701
701
702 # Returns an auto-generated project identifier based on the last identifier used
702 # Returns an auto-generated project identifier based on the last identifier used
703 def self.next_identifier
703 def self.next_identifier
704 p = Project.order('id DESC').first
704 p = Project.order('id DESC').first
705 p.nil? ? nil : p.identifier.to_s.succ
705 p.nil? ? nil : p.identifier.to_s.succ
706 end
706 end
707
707
708 # Copies and saves the Project instance based on the +project+.
708 # Copies and saves the Project instance based on the +project+.
709 # Duplicates the source project's:
709 # Duplicates the source project's:
710 # * Wiki
710 # * Wiki
711 # * Versions
711 # * Versions
712 # * Categories
712 # * Categories
713 # * Issues
713 # * Issues
714 # * Members
714 # * Members
715 # * Queries
715 # * Queries
716 #
716 #
717 # Accepts an +options+ argument to specify what to copy
717 # Accepts an +options+ argument to specify what to copy
718 #
718 #
719 # Examples:
719 # Examples:
720 # project.copy(1) # => copies everything
720 # project.copy(1) # => copies everything
721 # project.copy(1, :only => 'members') # => copies members only
721 # project.copy(1, :only => 'members') # => copies members only
722 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
722 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
723 def copy(project, options={})
723 def copy(project, options={})
724 project = project.is_a?(Project) ? project : Project.find(project)
724 project = project.is_a?(Project) ? project : Project.find(project)
725
725
726 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
726 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?
727 to_be_copied = to_be_copied & Array.wrap(options[:only]) unless options[:only].nil?
728
728
729 Project.transaction do
729 Project.transaction do
730 if save
730 if save
731 reload
731 reload
732 to_be_copied.each do |name|
732 to_be_copied.each do |name|
733 send "copy_#{name}", project
733 send "copy_#{name}", project
734 end
734 end
735 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
735 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
736 save
736 save
737 else
737 else
738 false
738 false
739 end
739 end
740 end
740 end
741 end
741 end
742
742
743 def member_principals
743 def member_principals
744 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
744 ActiveSupport::Deprecation.warn "Project#member_principals is deprecated and will be removed in Redmine 4.0. Use #memberships.active instead."
745 memberships.active
745 memberships.active
746 end
746 end
747
747
748 # Returns a new unsaved Project instance with attributes copied from +project+
748 # Returns a new unsaved Project instance with attributes copied from +project+
749 def self.copy_from(project)
749 def self.copy_from(project)
750 project = project.is_a?(Project) ? project : Project.find(project)
750 project = project.is_a?(Project) ? project : Project.find(project)
751 # clear unique attributes
751 # clear unique attributes
752 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
752 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
753 copy = Project.new(attributes)
753 copy = Project.new(attributes)
754 copy.enabled_modules = project.enabled_modules
754 copy.enabled_module_names = project.enabled_module_names
755 copy.trackers = project.trackers
755 copy.trackers = project.trackers
756 copy.custom_values = project.custom_values.collect {|v| v.clone}
756 copy.custom_values = project.custom_values.collect {|v| v.clone}
757 copy.issue_custom_fields = project.issue_custom_fields
757 copy.issue_custom_fields = project.issue_custom_fields
758 copy
758 copy
759 end
759 end
760
760
761 # Yields the given block for each project with its level in the tree
761 # Yields the given block for each project with its level in the tree
762 def self.project_tree(projects, &block)
762 def self.project_tree(projects, &block)
763 ancestors = []
763 ancestors = []
764 projects.sort_by(&:lft).each do |project|
764 projects.sort_by(&:lft).each do |project|
765 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
765 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
766 ancestors.pop
766 ancestors.pop
767 end
767 end
768 yield project, ancestors.size
768 yield project, ancestors.size
769 ancestors << project
769 ancestors << project
770 end
770 end
771 end
771 end
772
772
773 private
773 private
774
774
775 def update_inherited_members
775 def update_inherited_members
776 if parent
776 if parent
777 if inherit_members? && !inherit_members_was
777 if inherit_members? && !inherit_members_was
778 remove_inherited_member_roles
778 remove_inherited_member_roles
779 add_inherited_member_roles
779 add_inherited_member_roles
780 elsif !inherit_members? && inherit_members_was
780 elsif !inherit_members? && inherit_members_was
781 remove_inherited_member_roles
781 remove_inherited_member_roles
782 end
782 end
783 end
783 end
784 end
784 end
785
785
786 def remove_inherited_member_roles
786 def remove_inherited_member_roles
787 member_roles = memberships.map(&:member_roles).flatten
787 member_roles = memberships.map(&:member_roles).flatten
788 member_role_ids = member_roles.map(&:id)
788 member_role_ids = member_roles.map(&:id)
789 member_roles.each do |member_role|
789 member_roles.each do |member_role|
790 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
790 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
791 member_role.destroy
791 member_role.destroy
792 end
792 end
793 end
793 end
794 end
794 end
795
795
796 def add_inherited_member_roles
796 def add_inherited_member_roles
797 if inherit_members? && parent
797 if inherit_members? && parent
798 parent.memberships.each do |parent_member|
798 parent.memberships.each do |parent_member|
799 member = Member.find_or_new(self.id, parent_member.user_id)
799 member = Member.find_or_new(self.id, parent_member.user_id)
800 parent_member.member_roles.each do |parent_member_role|
800 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)
801 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
802 end
802 end
803 member.save!
803 member.save!
804 end
804 end
805 memberships.reset
805 memberships.reset
806 end
806 end
807 end
807 end
808
808
809 def update_versions_from_hierarchy_change
809 def update_versions_from_hierarchy_change
810 Issue.update_versions_from_hierarchy_change(self)
810 Issue.update_versions_from_hierarchy_change(self)
811 end
811 end
812
812
813 def validate_parent
813 def validate_parent
814 if @unallowed_parent_id
814 if @unallowed_parent_id
815 errors.add(:parent_id, :invalid)
815 errors.add(:parent_id, :invalid)
816 elsif parent_id_changed?
816 elsif parent_id_changed?
817 unless parent.nil? || (parent.active? && move_possible?(parent))
817 unless parent.nil? || (parent.active? && move_possible?(parent))
818 errors.add(:parent_id, :invalid)
818 errors.add(:parent_id, :invalid)
819 end
819 end
820 end
820 end
821 end
821 end
822
822
823 # Copies wiki from +project+
823 # Copies wiki from +project+
824 def copy_wiki(project)
824 def copy_wiki(project)
825 # Check that the source project has a wiki first
825 # Check that the source project has a wiki first
826 unless project.wiki.nil?
826 unless project.wiki.nil?
827 wiki = self.wiki || Wiki.new
827 wiki = self.wiki || Wiki.new
828 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
828 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
829 wiki_pages_map = {}
829 wiki_pages_map = {}
830 project.wiki.pages.each do |page|
830 project.wiki.pages.each do |page|
831 # Skip pages without content
831 # Skip pages without content
832 next if page.content.nil?
832 next if page.content.nil?
833 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
833 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"))
834 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
835 new_wiki_page.content = new_wiki_content
835 new_wiki_page.content = new_wiki_content
836 wiki.pages << new_wiki_page
836 wiki.pages << new_wiki_page
837 wiki_pages_map[page.id] = new_wiki_page
837 wiki_pages_map[page.id] = new_wiki_page
838 end
838 end
839
839
840 self.wiki = wiki
840 self.wiki = wiki
841 wiki.save
841 wiki.save
842 # Reproduce page hierarchy
842 # Reproduce page hierarchy
843 project.wiki.pages.each do |page|
843 project.wiki.pages.each do |page|
844 if page.parent_id && wiki_pages_map[page.id]
844 if page.parent_id && wiki_pages_map[page.id]
845 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
845 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
846 wiki_pages_map[page.id].save
846 wiki_pages_map[page.id].save
847 end
847 end
848 end
848 end
849 end
849 end
850 end
850 end
851
851
852 # Copies versions from +project+
852 # Copies versions from +project+
853 def copy_versions(project)
853 def copy_versions(project)
854 project.versions.each do |version|
854 project.versions.each do |version|
855 new_version = Version.new
855 new_version = Version.new
856 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
856 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
857 self.versions << new_version
857 self.versions << new_version
858 end
858 end
859 end
859 end
860
860
861 # Copies issue categories from +project+
861 # Copies issue categories from +project+
862 def copy_issue_categories(project)
862 def copy_issue_categories(project)
863 project.issue_categories.each do |issue_category|
863 project.issue_categories.each do |issue_category|
864 new_issue_category = IssueCategory.new
864 new_issue_category = IssueCategory.new
865 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
865 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
866 self.issue_categories << new_issue_category
866 self.issue_categories << new_issue_category
867 end
867 end
868 end
868 end
869
869
870 # Copies issues from +project+
870 # Copies issues from +project+
871 def copy_issues(project)
871 def copy_issues(project)
872 # Stores the source issue id as a key and the copied issues as the
872 # 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.
873 # value. Used to map the two together for issue relations.
874 issues_map = {}
874 issues_map = {}
875
875
876 # Store status and reopen locked/closed versions
876 # Store status and reopen locked/closed versions
877 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
877 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
878 version_statuses.each do |version, status|
878 version_statuses.each do |version, status|
879 version.update_attribute :status, 'open'
879 version.update_attribute :status, 'open'
880 end
880 end
881
881
882 # Get issues sorted by root_id, lft so that parent issues
882 # Get issues sorted by root_id, lft so that parent issues
883 # get copied before their children
883 # get copied before their children
884 project.issues.reorder('root_id, lft').each do |issue|
884 project.issues.reorder('root_id, lft').each do |issue|
885 new_issue = Issue.new
885 new_issue = Issue.new
886 new_issue.copy_from(issue, :subtasks => false, :link => false)
886 new_issue.copy_from(issue, :subtasks => false, :link => false)
887 new_issue.project = self
887 new_issue.project = self
888 # Changing project resets the custom field values
888 # Changing project resets the custom field values
889 # TODO: handle this in Issue#project=
889 # 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}
890 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
891 # Reassign fixed_versions by name, since names are unique per project
892 if issue.fixed_version && issue.fixed_version.project == project
892 if issue.fixed_version && issue.fixed_version.project == project
893 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
893 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
894 end
894 end
895 # Reassign the category by name, since names are unique per project
895 # Reassign the category by name, since names are unique per project
896 if issue.category
896 if issue.category
897 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
897 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
898 end
898 end
899 # Parent issue
899 # Parent issue
900 if issue.parent_id
900 if issue.parent_id
901 if copied_parent = issues_map[issue.parent_id]
901 if copied_parent = issues_map[issue.parent_id]
902 new_issue.parent_issue_id = copied_parent.id
902 new_issue.parent_issue_id = copied_parent.id
903 end
903 end
904 end
904 end
905
905
906 self.issues << new_issue
906 self.issues << new_issue
907 if new_issue.new_record?
907 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?
908 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info?
909 else
909 else
910 issues_map[issue.id] = new_issue unless new_issue.new_record?
910 issues_map[issue.id] = new_issue unless new_issue.new_record?
911 end
911 end
912 end
912 end
913
913
914 # Restore locked/closed version statuses
914 # Restore locked/closed version statuses
915 version_statuses.each do |version, status|
915 version_statuses.each do |version, status|
916 version.update_attribute :status, status
916 version.update_attribute :status, status
917 end
917 end
918
918
919 # Relations after in case issues related each other
919 # Relations after in case issues related each other
920 project.issues.each do |issue|
920 project.issues.each do |issue|
921 new_issue = issues_map[issue.id]
921 new_issue = issues_map[issue.id]
922 unless new_issue
922 unless new_issue
923 # Issue was not copied
923 # Issue was not copied
924 next
924 next
925 end
925 end
926
926
927 # Relations
927 # Relations
928 issue.relations_from.each do |source_relation|
928 issue.relations_from.each do |source_relation|
929 new_issue_relation = IssueRelation.new
929 new_issue_relation = IssueRelation.new
930 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
930 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]
931 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?
932 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
933 new_issue_relation.issue_to = source_relation.issue_to
933 new_issue_relation.issue_to = source_relation.issue_to
934 end
934 end
935 new_issue.relations_from << new_issue_relation
935 new_issue.relations_from << new_issue_relation
936 end
936 end
937
937
938 issue.relations_to.each do |source_relation|
938 issue.relations_to.each do |source_relation|
939 new_issue_relation = IssueRelation.new
939 new_issue_relation = IssueRelation.new
940 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
940 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]
941 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?
942 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
943 new_issue_relation.issue_from = source_relation.issue_from
943 new_issue_relation.issue_from = source_relation.issue_from
944 end
944 end
945 new_issue.relations_to << new_issue_relation
945 new_issue.relations_to << new_issue_relation
946 end
946 end
947 end
947 end
948 end
948 end
949
949
950 # Copies members from +project+
950 # Copies members from +project+
951 def copy_members(project)
951 def copy_members(project)
952 # Copy users first, then groups to handle members with inherited and given roles
952 # Copy users first, then groups to handle members with inherited and given roles
953 members_to_copy = []
953 members_to_copy = []
954 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
954 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)}
955 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
956
956
957 members_to_copy.each do |member|
957 members_to_copy.each do |member|
958 new_member = Member.new
958 new_member = Member.new
959 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
959 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
960 # only copy non inherited roles
960 # only copy non inherited roles
961 # inherited roles will be added when copying the group membership
961 # inherited roles will be added when copying the group membership
962 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
962 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
963 next if role_ids.empty?
963 next if role_ids.empty?
964 new_member.role_ids = role_ids
964 new_member.role_ids = role_ids
965 new_member.project = self
965 new_member.project = self
966 self.members << new_member
966 self.members << new_member
967 end
967 end
968 end
968 end
969
969
970 # Copies queries from +project+
970 # Copies queries from +project+
971 def copy_queries(project)
971 def copy_queries(project)
972 project.queries.each do |query|
972 project.queries.each do |query|
973 new_query = IssueQuery.new
973 new_query = IssueQuery.new
974 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria", "user_id", "type")
974 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
975 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
976 new_query.project = self
976 new_query.project = self
977 new_query.user_id = query.user_id
977 new_query.user_id = query.user_id
978 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
978 new_query.role_ids = query.role_ids if query.visibility == IssueQuery::VISIBILITY_ROLES
979 self.queries << new_query
979 self.queries << new_query
980 end
980 end
981 end
981 end
982
982
983 # Copies boards from +project+
983 # Copies boards from +project+
984 def copy_boards(project)
984 def copy_boards(project)
985 project.boards.each do |board|
985 project.boards.each do |board|
986 new_board = Board.new
986 new_board = Board.new
987 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
987 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
988 new_board.project = self
988 new_board.project = self
989 self.boards << new_board
989 self.boards << new_board
990 end
990 end
991 end
991 end
992
992
993 def allowed_permissions
993 def allowed_permissions
994 @allowed_permissions ||= begin
994 @allowed_permissions ||= begin
995 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
995 module_names = enabled_modules.loaded? ? enabled_modules.map(&:name) : enabled_modules.pluck(:name)
996 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
996 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
997 end
997 end
998 end
998 end
999
999
1000 def allowed_actions
1000 def allowed_actions
1001 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1001 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
1002 end
1002 end
1003
1003
1004 # Archives subprojects recursively
1004 # Archives subprojects recursively
1005 def archive!
1005 def archive!
1006 children.each do |subproject|
1006 children.each do |subproject|
1007 subproject.send :archive!
1007 subproject.send :archive!
1008 end
1008 end
1009 update_attribute :status, STATUS_ARCHIVED
1009 update_attribute :status, STATUS_ARCHIVED
1010 end
1010 end
1011 end
1011 end
@@ -1,975 +1,988
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 ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :projects, :trackers, :issue_statuses, :issues,
21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 :journals, :journal_details,
22 :journals, :journal_details,
23 :enumerations, :users, :issue_categories,
23 :enumerations, :users, :issue_categories,
24 :projects_trackers,
24 :projects_trackers,
25 :custom_fields,
25 :custom_fields,
26 :custom_fields_projects,
26 :custom_fields_projects,
27 :custom_fields_trackers,
27 :custom_fields_trackers,
28 :custom_values,
28 :custom_values,
29 :roles,
29 :roles,
30 :member_roles,
30 :member_roles,
31 :members,
31 :members,
32 :enabled_modules,
32 :enabled_modules,
33 :versions,
33 :versions,
34 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
34 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
35 :groups_users,
35 :groups_users,
36 :boards, :messages,
36 :boards, :messages,
37 :repositories,
37 :repositories,
38 :news, :comments,
38 :news, :comments,
39 :documents,
39 :documents,
40 :workflows
40 :workflows
41
41
42 def setup
42 def setup
43 @ecookbook = Project.find(1)
43 @ecookbook = Project.find(1)
44 @ecookbook_sub1 = Project.find(3)
44 @ecookbook_sub1 = Project.find(3)
45 set_tmp_attachments_directory
45 set_tmp_attachments_directory
46 User.current = nil
46 User.current = nil
47 end
47 end
48
48
49 def test_truth
49 def test_truth
50 assert_kind_of Project, @ecookbook
50 assert_kind_of Project, @ecookbook
51 assert_equal "eCookbook", @ecookbook.name
51 assert_equal "eCookbook", @ecookbook.name
52 end
52 end
53
53
54 def test_default_attributes
54 def test_default_attributes
55 with_settings :default_projects_public => '1' do
55 with_settings :default_projects_public => '1' do
56 assert_equal true, Project.new.is_public
56 assert_equal true, Project.new.is_public
57 assert_equal false, Project.new(:is_public => false).is_public
57 assert_equal false, Project.new(:is_public => false).is_public
58 end
58 end
59
59
60 with_settings :default_projects_public => '0' do
60 with_settings :default_projects_public => '0' do
61 assert_equal false, Project.new.is_public
61 assert_equal false, Project.new.is_public
62 assert_equal true, Project.new(:is_public => true).is_public
62 assert_equal true, Project.new(:is_public => true).is_public
63 end
63 end
64
64
65 with_settings :sequential_project_identifiers => '1' do
65 with_settings :sequential_project_identifiers => '1' do
66 assert !Project.new.identifier.blank?
66 assert !Project.new.identifier.blank?
67 assert Project.new(:identifier => '').identifier.blank?
67 assert Project.new(:identifier => '').identifier.blank?
68 end
68 end
69
69
70 with_settings :sequential_project_identifiers => '0' do
70 with_settings :sequential_project_identifiers => '0' do
71 assert Project.new.identifier.blank?
71 assert Project.new.identifier.blank?
72 assert !Project.new(:identifier => 'test').blank?
72 assert !Project.new(:identifier => 'test').blank?
73 end
73 end
74
74
75 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
75 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
76 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
76 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
77 end
77 end
78 end
78 end
79
79
80 def test_default_trackers_should_match_default_tracker_ids_setting
80 def test_default_trackers_should_match_default_tracker_ids_setting
81 with_settings :default_projects_tracker_ids => ['1', '3'] do
81 with_settings :default_projects_tracker_ids => ['1', '3'] do
82 assert_equal Tracker.find(1, 3).sort, Project.new.trackers.sort
82 assert_equal Tracker.find(1, 3).sort, Project.new.trackers.sort
83 end
83 end
84 end
84 end
85
85
86 def test_default_trackers_should_be_all_trackers_with_blank_setting
86 def test_default_trackers_should_be_all_trackers_with_blank_setting
87 with_settings :default_projects_tracker_ids => nil do
87 with_settings :default_projects_tracker_ids => nil do
88 assert_equal Tracker.all.sort, Project.new.trackers.sort
88 assert_equal Tracker.all.sort, Project.new.trackers.sort
89 end
89 end
90 end
90 end
91
91
92 def test_default_trackers_should_be_empty_with_empty_setting
92 def test_default_trackers_should_be_empty_with_empty_setting
93 with_settings :default_projects_tracker_ids => [] do
93 with_settings :default_projects_tracker_ids => [] do
94 assert_equal [], Project.new.trackers
94 assert_equal [], Project.new.trackers
95 end
95 end
96 end
96 end
97
97
98 def test_default_trackers_should_not_replace_initialized_trackers
98 def test_default_trackers_should_not_replace_initialized_trackers
99 with_settings :default_projects_tracker_ids => ['1', '3'] do
99 with_settings :default_projects_tracker_ids => ['1', '3'] do
100 assert_equal Tracker.find(1, 2).sort, Project.new(:tracker_ids => [1, 2]).trackers.sort
100 assert_equal Tracker.find(1, 2).sort, Project.new(:tracker_ids => [1, 2]).trackers.sort
101 end
101 end
102 end
102 end
103
103
104 def test_update
104 def test_update
105 assert_equal "eCookbook", @ecookbook.name
105 assert_equal "eCookbook", @ecookbook.name
106 @ecookbook.name = "eCook"
106 @ecookbook.name = "eCook"
107 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
107 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
108 @ecookbook.reload
108 @ecookbook.reload
109 assert_equal "eCook", @ecookbook.name
109 assert_equal "eCook", @ecookbook.name
110 end
110 end
111
111
112 def test_validate_identifier
112 def test_validate_identifier
113 to_test = {"abc" => true,
113 to_test = {"abc" => true,
114 "ab12" => true,
114 "ab12" => true,
115 "ab-12" => true,
115 "ab-12" => true,
116 "ab_12" => true,
116 "ab_12" => true,
117 "12" => false,
117 "12" => false,
118 "new" => false}
118 "new" => false}
119
119
120 to_test.each do |identifier, valid|
120 to_test.each do |identifier, valid|
121 p = Project.new
121 p = Project.new
122 p.identifier = identifier
122 p.identifier = identifier
123 p.valid?
123 p.valid?
124 if valid
124 if valid
125 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
125 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
126 else
126 else
127 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
127 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
128 end
128 end
129 end
129 end
130 end
130 end
131
131
132 def test_identifier_should_not_be_frozen_for_a_new_project
132 def test_identifier_should_not_be_frozen_for_a_new_project
133 assert_equal false, Project.new.identifier_frozen?
133 assert_equal false, Project.new.identifier_frozen?
134 end
134 end
135
135
136 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
136 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
137 Project.where(:id => 1).update_all(["identifier = ''"])
137 Project.where(:id => 1).update_all(["identifier = ''"])
138 assert_equal false, Project.find(1).identifier_frozen?
138 assert_equal false, Project.find(1).identifier_frozen?
139 end
139 end
140
140
141 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
141 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
142 assert_equal true, Project.find(1).identifier_frozen?
142 assert_equal true, Project.find(1).identifier_frozen?
143 end
143 end
144
144
145 def test_members_should_be_active_users
145 def test_members_should_be_active_users
146 Project.all.each do |project|
146 Project.all.each do |project|
147 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
147 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
148 end
148 end
149 end
149 end
150
150
151 def test_users_should_be_active_users
151 def test_users_should_be_active_users
152 Project.all.each do |project|
152 Project.all.each do |project|
153 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
153 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
154 end
154 end
155 end
155 end
156
156
157 def test_open_scope_on_issues_association
157 def test_open_scope_on_issues_association
158 assert_kind_of Issue, Project.find(1).issues.open.first
158 assert_kind_of Issue, Project.find(1).issues.open.first
159 end
159 end
160
160
161 def test_archive
161 def test_archive
162 user = @ecookbook.members.first.user
162 user = @ecookbook.members.first.user
163 @ecookbook.archive
163 @ecookbook.archive
164 @ecookbook.reload
164 @ecookbook.reload
165
165
166 assert !@ecookbook.active?
166 assert !@ecookbook.active?
167 assert @ecookbook.archived?
167 assert @ecookbook.archived?
168 assert !user.projects.include?(@ecookbook)
168 assert !user.projects.include?(@ecookbook)
169 # Subproject are also archived
169 # Subproject are also archived
170 assert !@ecookbook.children.empty?
170 assert !@ecookbook.children.empty?
171 assert @ecookbook.descendants.active.empty?
171 assert @ecookbook.descendants.active.empty?
172 end
172 end
173
173
174 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
174 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
175 # Assign an issue of a project to a version of a child project
175 # Assign an issue of a project to a version of a child project
176 Issue.find(4).update_attribute :fixed_version_id, 4
176 Issue.find(4).update_attribute :fixed_version_id, 4
177
177
178 assert_no_difference "Project.where(:status => Project::STATUS_ARCHIVED).count" do
178 assert_no_difference "Project.where(:status => Project::STATUS_ARCHIVED).count" do
179 assert_equal false, @ecookbook.archive
179 assert_equal false, @ecookbook.archive
180 end
180 end
181 @ecookbook.reload
181 @ecookbook.reload
182 assert @ecookbook.active?
182 assert @ecookbook.active?
183 end
183 end
184
184
185 def test_unarchive
185 def test_unarchive
186 user = @ecookbook.members.first.user
186 user = @ecookbook.members.first.user
187 @ecookbook.archive
187 @ecookbook.archive
188 # A subproject of an archived project can not be unarchived
188 # A subproject of an archived project can not be unarchived
189 assert !@ecookbook_sub1.unarchive
189 assert !@ecookbook_sub1.unarchive
190
190
191 # Unarchive project
191 # Unarchive project
192 assert @ecookbook.unarchive
192 assert @ecookbook.unarchive
193 @ecookbook.reload
193 @ecookbook.reload
194 assert @ecookbook.active?
194 assert @ecookbook.active?
195 assert !@ecookbook.archived?
195 assert !@ecookbook.archived?
196 assert user.projects.include?(@ecookbook)
196 assert user.projects.include?(@ecookbook)
197 # Subproject can now be unarchived
197 # Subproject can now be unarchived
198 @ecookbook_sub1.reload
198 @ecookbook_sub1.reload
199 assert @ecookbook_sub1.unarchive
199 assert @ecookbook_sub1.unarchive
200 end
200 end
201
201
202 def test_destroy
202 def test_destroy
203 # 2 active members
203 # 2 active members
204 assert_equal 2, @ecookbook.members.size
204 assert_equal 2, @ecookbook.members.size
205 # and 1 is locked
205 # and 1 is locked
206 assert_equal 3, Member.where(:project_id => @ecookbook.id).count
206 assert_equal 3, Member.where(:project_id => @ecookbook.id).count
207 # some boards
207 # some boards
208 assert @ecookbook.boards.any?
208 assert @ecookbook.boards.any?
209
209
210 @ecookbook.destroy
210 @ecookbook.destroy
211 # make sure that the project non longer exists
211 # make sure that the project non longer exists
212 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
212 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
213 # make sure related data was removed
213 # make sure related data was removed
214 assert_nil Member.where(:project_id => @ecookbook.id).first
214 assert_nil Member.where(:project_id => @ecookbook.id).first
215 assert_nil Board.where(:project_id => @ecookbook.id).first
215 assert_nil Board.where(:project_id => @ecookbook.id).first
216 assert_nil Issue.where(:project_id => @ecookbook.id).first
216 assert_nil Issue.where(:project_id => @ecookbook.id).first
217 end
217 end
218
218
219 def test_destroy_should_destroy_subtasks
219 def test_destroy_should_destroy_subtasks
220 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
220 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
221 issues[0].update_attribute :parent_issue_id, issues[1].id
221 issues[0].update_attribute :parent_issue_id, issues[1].id
222 issues[2].update_attribute :parent_issue_id, issues[1].id
222 issues[2].update_attribute :parent_issue_id, issues[1].id
223 assert_equal 2, issues[1].children.count
223 assert_equal 2, issues[1].children.count
224
224
225 assert_nothing_raised do
225 assert_nothing_raised do
226 Project.find(1).destroy
226 Project.find(1).destroy
227 end
227 end
228 assert_equal 0, Issue.where(:id => issues.map(&:id)).count
228 assert_equal 0, Issue.where(:id => issues.map(&:id)).count
229 end
229 end
230
230
231 def test_destroying_root_projects_should_clear_data
231 def test_destroying_root_projects_should_clear_data
232 Project.roots.each do |root|
232 Project.roots.each do |root|
233 root.destroy
233 root.destroy
234 end
234 end
235
235
236 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
236 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
237 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
237 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
238 assert_equal 0, MemberRole.count
238 assert_equal 0, MemberRole.count
239 assert_equal 0, Issue.count
239 assert_equal 0, Issue.count
240 assert_equal 0, Journal.count
240 assert_equal 0, Journal.count
241 assert_equal 0, JournalDetail.count
241 assert_equal 0, JournalDetail.count
242 assert_equal 0, Attachment.count, "Attachments were not deleted: #{Attachment.all.inspect}"
242 assert_equal 0, Attachment.count, "Attachments were not deleted: #{Attachment.all.inspect}"
243 assert_equal 0, EnabledModule.count
243 assert_equal 0, EnabledModule.count
244 assert_equal 0, IssueCategory.count
244 assert_equal 0, IssueCategory.count
245 assert_equal 0, IssueRelation.count
245 assert_equal 0, IssueRelation.count
246 assert_equal 0, Board.count
246 assert_equal 0, Board.count
247 assert_equal 0, Message.count
247 assert_equal 0, Message.count
248 assert_equal 0, News.count
248 assert_equal 0, News.count
249 assert_equal 0, Query.where("project_id IS NOT NULL").count
249 assert_equal 0, Query.where("project_id IS NOT NULL").count
250 assert_equal 0, Repository.count
250 assert_equal 0, Repository.count
251 assert_equal 0, Changeset.count
251 assert_equal 0, Changeset.count
252 assert_equal 0, Change.count
252 assert_equal 0, Change.count
253 assert_equal 0, Comment.count
253 assert_equal 0, Comment.count
254 assert_equal 0, TimeEntry.count
254 assert_equal 0, TimeEntry.count
255 assert_equal 0, Version.count
255 assert_equal 0, Version.count
256 assert_equal 0, Watcher.count
256 assert_equal 0, Watcher.count
257 assert_equal 0, Wiki.count
257 assert_equal 0, Wiki.count
258 assert_equal 0, WikiPage.count
258 assert_equal 0, WikiPage.count
259 assert_equal 0, WikiContent.count
259 assert_equal 0, WikiContent.count
260 assert_equal 0, WikiContent::Version.count
260 assert_equal 0, WikiContent::Version.count
261 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").count
261 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").count
262 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").count
262 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").count
263 assert_equal 0, CustomValue.where(:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']).count
263 assert_equal 0, CustomValue.where(:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']).count
264 end
264 end
265
265
266 def test_destroy_should_delete_time_entries_custom_values
266 def test_destroy_should_delete_time_entries_custom_values
267 project = Project.generate!
267 project = Project.generate!
268 time_entry = TimeEntry.generate!(:project => project, :custom_field_values => {10 => '1'})
268 time_entry = TimeEntry.generate!(:project => project, :custom_field_values => {10 => '1'})
269
269
270 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
270 assert_difference 'CustomValue.where(:customized_type => "TimeEntry").count', -1 do
271 assert project.destroy
271 assert project.destroy
272 end
272 end
273 end
273 end
274
274
275 def test_move_an_orphan_project_to_a_root_project
275 def test_move_an_orphan_project_to_a_root_project
276 sub = Project.find(2)
276 sub = Project.find(2)
277 sub.set_parent! @ecookbook
277 sub.set_parent! @ecookbook
278 assert_equal @ecookbook.id, sub.parent.id
278 assert_equal @ecookbook.id, sub.parent.id
279 @ecookbook.reload
279 @ecookbook.reload
280 assert_equal 4, @ecookbook.children.size
280 assert_equal 4, @ecookbook.children.size
281 end
281 end
282
282
283 def test_move_an_orphan_project_to_a_subproject
283 def test_move_an_orphan_project_to_a_subproject
284 sub = Project.find(2)
284 sub = Project.find(2)
285 assert sub.set_parent!(@ecookbook_sub1)
285 assert sub.set_parent!(@ecookbook_sub1)
286 end
286 end
287
287
288 def test_move_a_root_project_to_a_project
288 def test_move_a_root_project_to_a_project
289 sub = @ecookbook
289 sub = @ecookbook
290 assert sub.set_parent!(Project.find(2))
290 assert sub.set_parent!(Project.find(2))
291 end
291 end
292
292
293 def test_should_not_move_a_project_to_its_children
293 def test_should_not_move_a_project_to_its_children
294 sub = @ecookbook
294 sub = @ecookbook
295 assert !(sub.set_parent!(Project.find(3)))
295 assert !(sub.set_parent!(Project.find(3)))
296 end
296 end
297
297
298 def test_set_parent_should_add_roots_in_alphabetical_order
298 def test_set_parent_should_add_roots_in_alphabetical_order
299 ProjectCustomField.delete_all
299 ProjectCustomField.delete_all
300 Project.delete_all
300 Project.delete_all
301 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
301 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
302 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
302 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
303 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
303 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
304 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
304 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
305
305
306 assert_equal 4, Project.count
306 assert_equal 4, Project.count
307 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
307 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
308 end
308 end
309
309
310 def test_set_parent_should_add_children_in_alphabetical_order
310 def test_set_parent_should_add_children_in_alphabetical_order
311 ProjectCustomField.delete_all
311 ProjectCustomField.delete_all
312 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
312 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
313 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
313 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
314 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
314 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
315 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
315 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
316 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
316 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
317
317
318 parent.reload
318 parent.reload
319 assert_equal 4, parent.children.size
319 assert_equal 4, parent.children.size
320 assert_equal parent.children.sort_by(&:name), parent.children.to_a
320 assert_equal parent.children.sort_by(&:name), parent.children.to_a
321 end
321 end
322
322
323 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
323 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
324 # Parent issue with a hierarchy project's fixed version
324 # Parent issue with a hierarchy project's fixed version
325 parent_issue = Issue.find(1)
325 parent_issue = Issue.find(1)
326 parent_issue.update_attribute(:fixed_version_id, 4)
326 parent_issue.update_attribute(:fixed_version_id, 4)
327 parent_issue.reload
327 parent_issue.reload
328 assert_equal 4, parent_issue.fixed_version_id
328 assert_equal 4, parent_issue.fixed_version_id
329
329
330 # Should keep fixed versions for the issues
330 # Should keep fixed versions for the issues
331 issue_with_local_fixed_version = Issue.find(5)
331 issue_with_local_fixed_version = Issue.find(5)
332 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
332 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
333 issue_with_local_fixed_version.reload
333 issue_with_local_fixed_version.reload
334 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
334 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
335
335
336 # Local issue with hierarchy fixed_version
336 # Local issue with hierarchy fixed_version
337 issue_with_hierarchy_fixed_version = Issue.find(13)
337 issue_with_hierarchy_fixed_version = Issue.find(13)
338 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
338 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
339 issue_with_hierarchy_fixed_version.reload
339 issue_with_hierarchy_fixed_version.reload
340 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
340 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
341
341
342 # Move project out of the issue's hierarchy
342 # Move project out of the issue's hierarchy
343 moved_project = Project.find(3)
343 moved_project = Project.find(3)
344 moved_project.set_parent!(Project.find(2))
344 moved_project.set_parent!(Project.find(2))
345 parent_issue.reload
345 parent_issue.reload
346 issue_with_local_fixed_version.reload
346 issue_with_local_fixed_version.reload
347 issue_with_hierarchy_fixed_version.reload
347 issue_with_hierarchy_fixed_version.reload
348
348
349 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
349 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
350 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
350 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
351 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
351 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
352 end
352 end
353
353
354 def test_parent
354 def test_parent
355 p = Project.find(6).parent
355 p = Project.find(6).parent
356 assert p.is_a?(Project)
356 assert p.is_a?(Project)
357 assert_equal 5, p.id
357 assert_equal 5, p.id
358 end
358 end
359
359
360 def test_ancestors
360 def test_ancestors
361 a = Project.find(6).ancestors
361 a = Project.find(6).ancestors
362 assert a.first.is_a?(Project)
362 assert a.first.is_a?(Project)
363 assert_equal [1, 5], a.collect(&:id)
363 assert_equal [1, 5], a.collect(&:id)
364 end
364 end
365
365
366 def test_root
366 def test_root
367 r = Project.find(6).root
367 r = Project.find(6).root
368 assert r.is_a?(Project)
368 assert r.is_a?(Project)
369 assert_equal 1, r.id
369 assert_equal 1, r.id
370 end
370 end
371
371
372 def test_children
372 def test_children
373 c = Project.find(1).children
373 c = Project.find(1).children
374 assert c.first.is_a?(Project)
374 assert c.first.is_a?(Project)
375 assert_equal [5, 3, 4], c.collect(&:id)
375 assert_equal [5, 3, 4], c.collect(&:id)
376 end
376 end
377
377
378 def test_descendants
378 def test_descendants
379 d = Project.find(1).descendants
379 d = Project.find(1).descendants
380 assert d.first.is_a?(Project)
380 assert d.first.is_a?(Project)
381 assert_equal [5, 6, 3, 4], d.collect(&:id)
381 assert_equal [5, 6, 3, 4], d.collect(&:id)
382 end
382 end
383
383
384 def test_allowed_parents_should_be_empty_for_non_member_user
384 def test_allowed_parents_should_be_empty_for_non_member_user
385 Role.non_member.add_permission!(:add_project)
385 Role.non_member.add_permission!(:add_project)
386 user = User.find(9)
386 user = User.find(9)
387 assert user.memberships.empty?
387 assert user.memberships.empty?
388 User.current = user
388 User.current = user
389 assert Project.new.allowed_parents.compact.empty?
389 assert Project.new.allowed_parents.compact.empty?
390 end
390 end
391
391
392 def test_allowed_parents_with_add_subprojects_permission
392 def test_allowed_parents_with_add_subprojects_permission
393 Role.find(1).remove_permission!(:add_project)
393 Role.find(1).remove_permission!(:add_project)
394 Role.find(1).add_permission!(:add_subprojects)
394 Role.find(1).add_permission!(:add_subprojects)
395 User.current = User.find(2)
395 User.current = User.find(2)
396 # new project
396 # new project
397 assert !Project.new.allowed_parents.include?(nil)
397 assert !Project.new.allowed_parents.include?(nil)
398 assert Project.new.allowed_parents.include?(Project.find(1))
398 assert Project.new.allowed_parents.include?(Project.find(1))
399 # existing root project
399 # existing root project
400 assert Project.find(1).allowed_parents.include?(nil)
400 assert Project.find(1).allowed_parents.include?(nil)
401 # existing child
401 # existing child
402 assert Project.find(3).allowed_parents.include?(Project.find(1))
402 assert Project.find(3).allowed_parents.include?(Project.find(1))
403 assert !Project.find(3).allowed_parents.include?(nil)
403 assert !Project.find(3).allowed_parents.include?(nil)
404 end
404 end
405
405
406 def test_allowed_parents_with_add_project_permission
406 def test_allowed_parents_with_add_project_permission
407 Role.find(1).add_permission!(:add_project)
407 Role.find(1).add_permission!(:add_project)
408 Role.find(1).remove_permission!(:add_subprojects)
408 Role.find(1).remove_permission!(:add_subprojects)
409 User.current = User.find(2)
409 User.current = User.find(2)
410 # new project
410 # new project
411 assert Project.new.allowed_parents.include?(nil)
411 assert Project.new.allowed_parents.include?(nil)
412 assert !Project.new.allowed_parents.include?(Project.find(1))
412 assert !Project.new.allowed_parents.include?(Project.find(1))
413 # existing root project
413 # existing root project
414 assert Project.find(1).allowed_parents.include?(nil)
414 assert Project.find(1).allowed_parents.include?(nil)
415 # existing child
415 # existing child
416 assert Project.find(3).allowed_parents.include?(Project.find(1))
416 assert Project.find(3).allowed_parents.include?(Project.find(1))
417 assert Project.find(3).allowed_parents.include?(nil)
417 assert Project.find(3).allowed_parents.include?(nil)
418 end
418 end
419
419
420 def test_allowed_parents_with_add_project_and_subprojects_permission
420 def test_allowed_parents_with_add_project_and_subprojects_permission
421 Role.find(1).add_permission!(:add_project)
421 Role.find(1).add_permission!(:add_project)
422 Role.find(1).add_permission!(:add_subprojects)
422 Role.find(1).add_permission!(:add_subprojects)
423 User.current = User.find(2)
423 User.current = User.find(2)
424 # new project
424 # new project
425 assert Project.new.allowed_parents.include?(nil)
425 assert Project.new.allowed_parents.include?(nil)
426 assert Project.new.allowed_parents.include?(Project.find(1))
426 assert Project.new.allowed_parents.include?(Project.find(1))
427 # existing root project
427 # existing root project
428 assert Project.find(1).allowed_parents.include?(nil)
428 assert Project.find(1).allowed_parents.include?(nil)
429 # existing child
429 # existing child
430 assert Project.find(3).allowed_parents.include?(Project.find(1))
430 assert Project.find(3).allowed_parents.include?(Project.find(1))
431 assert Project.find(3).allowed_parents.include?(nil)
431 assert Project.find(3).allowed_parents.include?(nil)
432 end
432 end
433
433
434 def test_users_by_role
434 def test_users_by_role
435 users_by_role = Project.find(1).users_by_role
435 users_by_role = Project.find(1).users_by_role
436 assert_kind_of Hash, users_by_role
436 assert_kind_of Hash, users_by_role
437 role = Role.find(1)
437 role = Role.find(1)
438 assert_kind_of Array, users_by_role[role]
438 assert_kind_of Array, users_by_role[role]
439 assert users_by_role[role].include?(User.find(2))
439 assert users_by_role[role].include?(User.find(2))
440 end
440 end
441
441
442 def test_rolled_up_trackers
442 def test_rolled_up_trackers
443 parent = Project.find(1)
443 parent = Project.find(1)
444 parent.trackers = Tracker.find([1,2])
444 parent.trackers = Tracker.find([1,2])
445 child = parent.children.find(3)
445 child = parent.children.find(3)
446
446
447 assert_equal [1, 2], parent.tracker_ids
447 assert_equal [1, 2], parent.tracker_ids
448 assert_equal [2, 3], child.trackers.collect(&:id)
448 assert_equal [2, 3], child.trackers.collect(&:id)
449
449
450 assert_kind_of Tracker, parent.rolled_up_trackers.first
450 assert_kind_of Tracker, parent.rolled_up_trackers.first
451 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
451 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
452
452
453 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
453 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
454 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
454 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
455 end
455 end
456
456
457 def test_rolled_up_trackers_should_ignore_archived_subprojects
457 def test_rolled_up_trackers_should_ignore_archived_subprojects
458 parent = Project.find(1)
458 parent = Project.find(1)
459 parent.trackers = Tracker.find([1,2])
459 parent.trackers = Tracker.find([1,2])
460 child = parent.children.find(3)
460 child = parent.children.find(3)
461 child.trackers = Tracker.find([1,3])
461 child.trackers = Tracker.find([1,3])
462 parent.children.each(&:archive)
462 parent.children.each(&:archive)
463
463
464 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
464 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
465 end
465 end
466
466
467 test "#rolled_up_trackers should ignore projects with issue_tracking module disabled" do
467 test "#rolled_up_trackers should ignore projects with issue_tracking module disabled" do
468 parent = Project.generate!
468 parent = Project.generate!
469 parent.trackers = Tracker.find([1, 2])
469 parent.trackers = Tracker.find([1, 2])
470 child = Project.generate_with_parent!(parent)
470 child = Project.generate_with_parent!(parent)
471 child.trackers = Tracker.find([2, 3])
471 child.trackers = Tracker.find([2, 3])
472
472
473 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id).sort
473 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id).sort
474
474
475 assert child.disable_module!(:issue_tracking)
475 assert child.disable_module!(:issue_tracking)
476 parent.reload
476 parent.reload
477 assert_equal [1, 2], parent.rolled_up_trackers.collect(&:id).sort
477 assert_equal [1, 2], parent.rolled_up_trackers.collect(&:id).sort
478 end
478 end
479
479
480 test "#rolled_up_versions should include the versions for the current project" do
480 test "#rolled_up_versions should include the versions for the current project" do
481 project = Project.generate!
481 project = Project.generate!
482 parent_version_1 = Version.generate!(:project => project)
482 parent_version_1 = Version.generate!(:project => project)
483 parent_version_2 = Version.generate!(:project => project)
483 parent_version_2 = Version.generate!(:project => project)
484 assert_equal [parent_version_1, parent_version_2].sort,
484 assert_equal [parent_version_1, parent_version_2].sort,
485 project.rolled_up_versions.sort
485 project.rolled_up_versions.sort
486 end
486 end
487
487
488 test "#rolled_up_versions should include versions for a subproject" do
488 test "#rolled_up_versions should include versions for a subproject" do
489 project = Project.generate!
489 project = Project.generate!
490 parent_version_1 = Version.generate!(:project => project)
490 parent_version_1 = Version.generate!(:project => project)
491 parent_version_2 = Version.generate!(:project => project)
491 parent_version_2 = Version.generate!(:project => project)
492 subproject = Project.generate_with_parent!(project)
492 subproject = Project.generate_with_parent!(project)
493 subproject_version = Version.generate!(:project => subproject)
493 subproject_version = Version.generate!(:project => subproject)
494
494
495 assert_equal [parent_version_1, parent_version_2, subproject_version].sort,
495 assert_equal [parent_version_1, parent_version_2, subproject_version].sort,
496 project.rolled_up_versions.sort
496 project.rolled_up_versions.sort
497 end
497 end
498
498
499 test "#rolled_up_versions should include versions for a sub-subproject" do
499 test "#rolled_up_versions should include versions for a sub-subproject" do
500 project = Project.generate!
500 project = Project.generate!
501 parent_version_1 = Version.generate!(:project => project)
501 parent_version_1 = Version.generate!(:project => project)
502 parent_version_2 = Version.generate!(:project => project)
502 parent_version_2 = Version.generate!(:project => project)
503 subproject = Project.generate_with_parent!(project)
503 subproject = Project.generate_with_parent!(project)
504 sub_subproject = Project.generate_with_parent!(subproject)
504 sub_subproject = Project.generate_with_parent!(subproject)
505 sub_subproject_version = Version.generate!(:project => sub_subproject)
505 sub_subproject_version = Version.generate!(:project => sub_subproject)
506 project.reload
506 project.reload
507
507
508 assert_equal [parent_version_1, parent_version_2, sub_subproject_version].sort,
508 assert_equal [parent_version_1, parent_version_2, sub_subproject_version].sort,
509 project.rolled_up_versions.sort
509 project.rolled_up_versions.sort
510 end
510 end
511
511
512 test "#rolled_up_versions should only check active projects" do
512 test "#rolled_up_versions should only check active projects" do
513 project = Project.generate!
513 project = Project.generate!
514 parent_version_1 = Version.generate!(:project => project)
514 parent_version_1 = Version.generate!(:project => project)
515 parent_version_2 = Version.generate!(:project => project)
515 parent_version_2 = Version.generate!(:project => project)
516 subproject = Project.generate_with_parent!(project)
516 subproject = Project.generate_with_parent!(project)
517 subproject_version = Version.generate!(:project => subproject)
517 subproject_version = Version.generate!(:project => subproject)
518 assert subproject.archive
518 assert subproject.archive
519 project.reload
519 project.reload
520
520
521 assert !subproject.active?
521 assert !subproject.active?
522 assert_equal [parent_version_1, parent_version_2].sort,
522 assert_equal [parent_version_1, parent_version_2].sort,
523 project.rolled_up_versions.sort
523 project.rolled_up_versions.sort
524 end
524 end
525
525
526 def test_shared_versions_none_sharing
526 def test_shared_versions_none_sharing
527 p = Project.find(5)
527 p = Project.find(5)
528 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
528 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
529 assert p.shared_versions.include?(v)
529 assert p.shared_versions.include?(v)
530 assert !p.children.first.shared_versions.include?(v)
530 assert !p.children.first.shared_versions.include?(v)
531 assert !p.root.shared_versions.include?(v)
531 assert !p.root.shared_versions.include?(v)
532 assert !p.siblings.first.shared_versions.include?(v)
532 assert !p.siblings.first.shared_versions.include?(v)
533 assert !p.root.siblings.first.shared_versions.include?(v)
533 assert !p.root.siblings.first.shared_versions.include?(v)
534 end
534 end
535
535
536 def test_shared_versions_descendants_sharing
536 def test_shared_versions_descendants_sharing
537 p = Project.find(5)
537 p = Project.find(5)
538 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
538 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
539 assert p.shared_versions.include?(v)
539 assert p.shared_versions.include?(v)
540 assert p.children.first.shared_versions.include?(v)
540 assert p.children.first.shared_versions.include?(v)
541 assert !p.root.shared_versions.include?(v)
541 assert !p.root.shared_versions.include?(v)
542 assert !p.siblings.first.shared_versions.include?(v)
542 assert !p.siblings.first.shared_versions.include?(v)
543 assert !p.root.siblings.first.shared_versions.include?(v)
543 assert !p.root.siblings.first.shared_versions.include?(v)
544 end
544 end
545
545
546 def test_shared_versions_hierarchy_sharing
546 def test_shared_versions_hierarchy_sharing
547 p = Project.find(5)
547 p = Project.find(5)
548 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
548 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
549 assert p.shared_versions.include?(v)
549 assert p.shared_versions.include?(v)
550 assert p.children.first.shared_versions.include?(v)
550 assert p.children.first.shared_versions.include?(v)
551 assert p.root.shared_versions.include?(v)
551 assert p.root.shared_versions.include?(v)
552 assert !p.siblings.first.shared_versions.include?(v)
552 assert !p.siblings.first.shared_versions.include?(v)
553 assert !p.root.siblings.first.shared_versions.include?(v)
553 assert !p.root.siblings.first.shared_versions.include?(v)
554 end
554 end
555
555
556 def test_shared_versions_tree_sharing
556 def test_shared_versions_tree_sharing
557 p = Project.find(5)
557 p = Project.find(5)
558 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
558 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
559 assert p.shared_versions.include?(v)
559 assert p.shared_versions.include?(v)
560 assert p.children.first.shared_versions.include?(v)
560 assert p.children.first.shared_versions.include?(v)
561 assert p.root.shared_versions.include?(v)
561 assert p.root.shared_versions.include?(v)
562 assert p.siblings.first.shared_versions.include?(v)
562 assert p.siblings.first.shared_versions.include?(v)
563 assert !p.root.siblings.first.shared_versions.include?(v)
563 assert !p.root.siblings.first.shared_versions.include?(v)
564 end
564 end
565
565
566 def test_shared_versions_system_sharing
566 def test_shared_versions_system_sharing
567 p = Project.find(5)
567 p = Project.find(5)
568 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
568 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
569 assert p.shared_versions.include?(v)
569 assert p.shared_versions.include?(v)
570 assert p.children.first.shared_versions.include?(v)
570 assert p.children.first.shared_versions.include?(v)
571 assert p.root.shared_versions.include?(v)
571 assert p.root.shared_versions.include?(v)
572 assert p.siblings.first.shared_versions.include?(v)
572 assert p.siblings.first.shared_versions.include?(v)
573 assert p.root.siblings.first.shared_versions.include?(v)
573 assert p.root.siblings.first.shared_versions.include?(v)
574 end
574 end
575
575
576 def test_shared_versions
576 def test_shared_versions
577 parent = Project.find(1)
577 parent = Project.find(1)
578 child = parent.children.find(3)
578 child = parent.children.find(3)
579 private_child = parent.children.find(5)
579 private_child = parent.children.find(5)
580
580
581 assert_equal [1,2,3], parent.version_ids.sort
581 assert_equal [1,2,3], parent.version_ids.sort
582 assert_equal [4], child.version_ids
582 assert_equal [4], child.version_ids
583 assert_equal [6], private_child.version_ids
583 assert_equal [6], private_child.version_ids
584 assert_equal [7], Version.where(:sharing => 'system').collect(&:id)
584 assert_equal [7], Version.where(:sharing => 'system').collect(&:id)
585
585
586 assert_equal 6, parent.shared_versions.size
586 assert_equal 6, parent.shared_versions.size
587 parent.shared_versions.each do |version|
587 parent.shared_versions.each do |version|
588 assert_kind_of Version, version
588 assert_kind_of Version, version
589 end
589 end
590
590
591 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
591 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
592 end
592 end
593
593
594 def test_shared_versions_should_ignore_archived_subprojects
594 def test_shared_versions_should_ignore_archived_subprojects
595 parent = Project.find(1)
595 parent = Project.find(1)
596 child = parent.children.find(3)
596 child = parent.children.find(3)
597 child.archive
597 child.archive
598 parent.reload
598 parent.reload
599
599
600 assert_equal [1,2,3], parent.version_ids.sort
600 assert_equal [1,2,3], parent.version_ids.sort
601 assert_equal [4], child.version_ids
601 assert_equal [4], child.version_ids
602 assert !parent.shared_versions.collect(&:id).include?(4)
602 assert !parent.shared_versions.collect(&:id).include?(4)
603 end
603 end
604
604
605 def test_shared_versions_visible_to_user
605 def test_shared_versions_visible_to_user
606 user = User.find(3)
606 user = User.find(3)
607 parent = Project.find(1)
607 parent = Project.find(1)
608 child = parent.children.find(5)
608 child = parent.children.find(5)
609
609
610 assert_equal [1,2,3], parent.version_ids.sort
610 assert_equal [1,2,3], parent.version_ids.sort
611 assert_equal [6], child.version_ids
611 assert_equal [6], child.version_ids
612
612
613 versions = parent.shared_versions.visible(user)
613 versions = parent.shared_versions.visible(user)
614
614
615 assert_equal 4, versions.size
615 assert_equal 4, versions.size
616 versions.each do |version|
616 versions.each do |version|
617 assert_kind_of Version, version
617 assert_kind_of Version, version
618 end
618 end
619
619
620 assert !versions.collect(&:id).include?(6)
620 assert !versions.collect(&:id).include?(6)
621 end
621 end
622
622
623 def test_shared_versions_for_new_project_should_include_system_shared_versions
623 def test_shared_versions_for_new_project_should_include_system_shared_versions
624 p = Project.find(5)
624 p = Project.find(5)
625 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
625 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
626
626
627 assert_include v, Project.new.shared_versions
627 assert_include v, Project.new.shared_versions
628 end
628 end
629
629
630 def test_next_identifier
630 def test_next_identifier
631 ProjectCustomField.delete_all
631 ProjectCustomField.delete_all
632 Project.create!(:name => 'last', :identifier => 'p2008040')
632 Project.create!(:name => 'last', :identifier => 'p2008040')
633 assert_equal 'p2008041', Project.next_identifier
633 assert_equal 'p2008041', Project.next_identifier
634 end
634 end
635
635
636 def test_next_identifier_first_project
636 def test_next_identifier_first_project
637 Project.delete_all
637 Project.delete_all
638 assert_nil Project.next_identifier
638 assert_nil Project.next_identifier
639 end
639 end
640
640
641 def test_enabled_module_names
641 def test_enabled_module_names
642 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
642 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
643 project = Project.new
643 project = Project.new
644
644
645 project.enabled_module_names = %w(issue_tracking news)
645 project.enabled_module_names = %w(issue_tracking news)
646 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
646 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
647 end
647 end
648 end
648 end
649
649
650 def test_enabled_modules_names_with_nil_should_clear_modules
650 def test_enabled_modules_names_with_nil_should_clear_modules
651 p = Project.find(1)
651 p = Project.find(1)
652 p.enabled_module_names = nil
652 p.enabled_module_names = nil
653 assert_equal [], p.enabled_modules
653 assert_equal [], p.enabled_modules
654 end
654 end
655
655
656 test "enabled_modules should define module by names and preserve ids" do
656 test "enabled_modules should define module by names and preserve ids" do
657 @project = Project.find(1)
657 @project = Project.find(1)
658 # Remove one module
658 # Remove one module
659 modules = @project.enabled_modules.slice(0..-2)
659 modules = @project.enabled_modules.slice(0..-2)
660 assert modules.any?
660 assert modules.any?
661 assert_difference 'EnabledModule.count', -1 do
661 assert_difference 'EnabledModule.count', -1 do
662 @project.enabled_module_names = modules.collect(&:name)
662 @project.enabled_module_names = modules.collect(&:name)
663 end
663 end
664 @project.reload
664 @project.reload
665 # Ids should be preserved
665 # Ids should be preserved
666 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
666 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
667 end
667 end
668
668
669 test "enabled_modules should enable a module" do
669 test "enabled_modules should enable a module" do
670 @project = Project.find(1)
670 @project = Project.find(1)
671 @project.enabled_module_names = []
671 @project.enabled_module_names = []
672 @project.reload
672 @project.reload
673 assert_equal [], @project.enabled_module_names
673 assert_equal [], @project.enabled_module_names
674 #with string
674 #with string
675 @project.enable_module!("issue_tracking")
675 @project.enable_module!("issue_tracking")
676 assert_equal ["issue_tracking"], @project.enabled_module_names
676 assert_equal ["issue_tracking"], @project.enabled_module_names
677 #with symbol
677 #with symbol
678 @project.enable_module!(:gantt)
678 @project.enable_module!(:gantt)
679 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
679 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
680 #don't add a module twice
680 #don't add a module twice
681 @project.enable_module!("issue_tracking")
681 @project.enable_module!("issue_tracking")
682 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
682 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
683 end
683 end
684
684
685 test "enabled_modules should disable a module" do
685 test "enabled_modules should disable a module" do
686 @project = Project.find(1)
686 @project = Project.find(1)
687 #with string
687 #with string
688 assert @project.enabled_module_names.include?("issue_tracking")
688 assert @project.enabled_module_names.include?("issue_tracking")
689 @project.disable_module!("issue_tracking")
689 @project.disable_module!("issue_tracking")
690 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
690 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
691 #with symbol
691 #with symbol
692 assert @project.enabled_module_names.include?("gantt")
692 assert @project.enabled_module_names.include?("gantt")
693 @project.disable_module!(:gantt)
693 @project.disable_module!(:gantt)
694 assert ! @project.reload.enabled_module_names.include?("gantt")
694 assert ! @project.reload.enabled_module_names.include?("gantt")
695 #with EnabledModule object
695 #with EnabledModule object
696 first_module = @project.enabled_modules.first
696 first_module = @project.enabled_modules.first
697 @project.disable_module!(first_module)
697 @project.disable_module!(first_module)
698 assert ! @project.reload.enabled_module_names.include?(first_module.name)
698 assert ! @project.reload.enabled_module_names.include?(first_module.name)
699 end
699 end
700
700
701 def test_enabled_module_names_should_not_recreate_enabled_modules
701 def test_enabled_module_names_should_not_recreate_enabled_modules
702 project = Project.find(1)
702 project = Project.find(1)
703 # Remove one module
703 # Remove one module
704 modules = project.enabled_modules.slice(0..-2)
704 modules = project.enabled_modules.slice(0..-2)
705 assert modules.any?
705 assert modules.any?
706 assert_difference 'EnabledModule.count', -1 do
706 assert_difference 'EnabledModule.count', -1 do
707 project.enabled_module_names = modules.collect(&:name)
707 project.enabled_module_names = modules.collect(&:name)
708 end
708 end
709 project.reload
709 project.reload
710 # Ids should be preserved
710 # Ids should be preserved
711 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
711 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
712 end
712 end
713
713
714 def test_copy_from_existing_project
714 def test_copy_from_existing_project
715 source_project = Project.find(1)
715 source_project = Project.find(1)
716 copied_project = Project.copy_from(1)
716 copied_project = Project.copy_from(1)
717
717
718 assert copied_project
718 assert copied_project
719 # Cleared attributes
719 # Cleared attributes
720 assert copied_project.id.blank?
720 assert copied_project.id.blank?
721 assert copied_project.name.blank?
721 assert copied_project.name.blank?
722 assert copied_project.identifier.blank?
722 assert copied_project.identifier.blank?
723
723
724 # Duplicated attributes
724 # Duplicated attributes
725 assert_equal source_project.description, copied_project.description
725 assert_equal source_project.description, copied_project.description
726 assert_equal source_project.enabled_modules, copied_project.enabled_modules
727 assert_equal source_project.trackers, copied_project.trackers
726 assert_equal source_project.trackers, copied_project.trackers
728
727
729 # Default attributes
728 # Default attributes
730 assert_equal 1, copied_project.status
729 assert_equal 1, copied_project.status
731 end
730 end
732
731
732 def test_copy_from_should_copy_enabled_modules
733 source = Project.generate!
734 source.enabled_module_names = %w(issue_tracking wiki)
735
736 copy = Project.copy_from(source)
737 copy.name = 'Copy'
738 copy.identifier = 'copy'
739 assert_difference 'EnabledModule.count', 2 do
740 copy.save!
741 end
742 assert_equal 2, copy.reload.enabled_modules.count
743 assert_equal 2, source.reload.enabled_modules.count
744 end
745
733 def test_activities_should_use_the_system_activities
746 def test_activities_should_use_the_system_activities
734 project = Project.find(1)
747 project = Project.find(1)
735 assert_equal project.activities.to_a, TimeEntryActivity.where(:active => true).to_a
748 assert_equal project.activities.to_a, TimeEntryActivity.where(:active => true).to_a
736 assert_kind_of ActiveRecord::Relation, project.activities
749 assert_kind_of ActiveRecord::Relation, project.activities
737 end
750 end
738
751
739
752
740 def test_activities_should_use_the_project_specific_activities
753 def test_activities_should_use_the_project_specific_activities
741 project = Project.find(1)
754 project = Project.find(1)
742 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
755 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
743 assert overridden_activity.save!
756 assert overridden_activity.save!
744
757
745 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
758 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
746 assert_kind_of ActiveRecord::Relation, project.activities
759 assert_kind_of ActiveRecord::Relation, project.activities
747 end
760 end
748
761
749 def test_activities_should_not_include_the_inactive_project_specific_activities
762 def test_activities_should_not_include_the_inactive_project_specific_activities
750 project = Project.find(1)
763 project = Project.find(1)
751 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
764 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
752 assert overridden_activity.save!
765 assert overridden_activity.save!
753
766
754 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
767 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
755 end
768 end
756
769
757 def test_activities_should_not_include_project_specific_activities_from_other_projects
770 def test_activities_should_not_include_project_specific_activities_from_other_projects
758 project = Project.find(1)
771 project = Project.find(1)
759 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
772 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
760 assert overridden_activity.save!
773 assert overridden_activity.save!
761
774
762 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
775 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
763 end
776 end
764
777
765 def test_activities_should_handle_nils
778 def test_activities_should_handle_nils
766 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.first})
779 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.first})
767 TimeEntryActivity.delete_all
780 TimeEntryActivity.delete_all
768
781
769 # No activities
782 # No activities
770 project = Project.find(1)
783 project = Project.find(1)
771 assert project.activities.empty?
784 assert project.activities.empty?
772
785
773 # No system, one overridden
786 # No system, one overridden
774 assert overridden_activity.save!
787 assert overridden_activity.save!
775 project.reload
788 project.reload
776 assert_equal [overridden_activity], project.activities
789 assert_equal [overridden_activity], project.activities
777 end
790 end
778
791
779 def test_activities_should_override_system_activities_with_project_activities
792 def test_activities_should_override_system_activities_with_project_activities
780 project = Project.find(1)
793 project = Project.find(1)
781 parent_activity = TimeEntryActivity.first
794 parent_activity = TimeEntryActivity.first
782 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
795 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
783 assert overridden_activity.save!
796 assert overridden_activity.save!
784
797
785 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
798 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
786 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
799 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
787 end
800 end
788
801
789 def test_activities_should_include_inactive_activities_if_specified
802 def test_activities_should_include_inactive_activities_if_specified
790 project = Project.find(1)
803 project = Project.find(1)
791 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
804 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
792 assert overridden_activity.save!
805 assert overridden_activity.save!
793
806
794 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
807 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
795 end
808 end
796
809
797 test 'activities should not include active System activities if the project has an override that is inactive' do
810 test 'activities should not include active System activities if the project has an override that is inactive' do
798 project = Project.find(1)
811 project = Project.find(1)
799 system_activity = TimeEntryActivity.find_by_name('Design')
812 system_activity = TimeEntryActivity.find_by_name('Design')
800 assert system_activity.active?
813 assert system_activity.active?
801 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
814 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
802 assert overridden_activity.save!
815 assert overridden_activity.save!
803
816
804 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
817 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
805 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
818 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
806 end
819 end
807
820
808 def test_close_completed_versions
821 def test_close_completed_versions
809 Version.update_all("status = 'open'")
822 Version.update_all("status = 'open'")
810 project = Project.find(1)
823 project = Project.find(1)
811 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
824 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
812 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
825 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
813 project.close_completed_versions
826 project.close_completed_versions
814 project.reload
827 project.reload
815 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
828 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
816 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
829 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
817 end
830 end
818
831
819 test "#start_date should be nil if there are no issues on the project" do
832 test "#start_date should be nil if there are no issues on the project" do
820 project = Project.generate!
833 project = Project.generate!
821 assert_nil project.start_date
834 assert_nil project.start_date
822 end
835 end
823
836
824 test "#start_date should be nil when issues have no start date" do
837 test "#start_date should be nil when issues have no start date" do
825 project = Project.generate!
838 project = Project.generate!
826 project.trackers << Tracker.generate!
839 project.trackers << Tracker.generate!
827 early = 7.days.ago.to_date
840 early = 7.days.ago.to_date
828 Issue.generate!(:project => project, :start_date => nil)
841 Issue.generate!(:project => project, :start_date => nil)
829
842
830 assert_nil project.start_date
843 assert_nil project.start_date
831 end
844 end
832
845
833 test "#start_date should be the earliest start date of it's issues" do
846 test "#start_date should be the earliest start date of it's issues" do
834 project = Project.generate!
847 project = Project.generate!
835 project.trackers << Tracker.generate!
848 project.trackers << Tracker.generate!
836 early = 7.days.ago.to_date
849 early = 7.days.ago.to_date
837 Issue.generate!(:project => project, :start_date => Date.today)
850 Issue.generate!(:project => project, :start_date => Date.today)
838 Issue.generate!(:project => project, :start_date => early)
851 Issue.generate!(:project => project, :start_date => early)
839
852
840 assert_equal early, project.start_date
853 assert_equal early, project.start_date
841 end
854 end
842
855
843 test "#due_date should be nil if there are no issues on the project" do
856 test "#due_date should be nil if there are no issues on the project" do
844 project = Project.generate!
857 project = Project.generate!
845 assert_nil project.due_date
858 assert_nil project.due_date
846 end
859 end
847
860
848 test "#due_date should be nil if there are no issues with due dates" do
861 test "#due_date should be nil if there are no issues with due dates" do
849 project = Project.generate!
862 project = Project.generate!
850 project.trackers << Tracker.generate!
863 project.trackers << Tracker.generate!
851 Issue.generate!(:project => project, :due_date => nil)
864 Issue.generate!(:project => project, :due_date => nil)
852
865
853 assert_nil project.due_date
866 assert_nil project.due_date
854 end
867 end
855
868
856 test "#due_date should be the latest due date of it's issues" do
869 test "#due_date should be the latest due date of it's issues" do
857 project = Project.generate!
870 project = Project.generate!
858 project.trackers << Tracker.generate!
871 project.trackers << Tracker.generate!
859 future = 7.days.from_now.to_date
872 future = 7.days.from_now.to_date
860 Issue.generate!(:project => project, :due_date => future)
873 Issue.generate!(:project => project, :due_date => future)
861 Issue.generate!(:project => project, :due_date => Date.today)
874 Issue.generate!(:project => project, :due_date => Date.today)
862
875
863 assert_equal future, project.due_date
876 assert_equal future, project.due_date
864 end
877 end
865
878
866 test "#due_date should be the latest due date of it's versions" do
879 test "#due_date should be the latest due date of it's versions" do
867 project = Project.generate!
880 project = Project.generate!
868 future = 7.days.from_now.to_date
881 future = 7.days.from_now.to_date
869 project.versions << Version.generate!(:effective_date => future)
882 project.versions << Version.generate!(:effective_date => future)
870 project.versions << Version.generate!(:effective_date => Date.today)
883 project.versions << Version.generate!(:effective_date => Date.today)
871
884
872 assert_equal future, project.due_date
885 assert_equal future, project.due_date
873 end
886 end
874
887
875 test "#due_date should pick the latest date from it's issues and versions" do
888 test "#due_date should pick the latest date from it's issues and versions" do
876 project = Project.generate!
889 project = Project.generate!
877 project.trackers << Tracker.generate!
890 project.trackers << Tracker.generate!
878 future = 7.days.from_now.to_date
891 future = 7.days.from_now.to_date
879 far_future = 14.days.from_now.to_date
892 far_future = 14.days.from_now.to_date
880 Issue.generate!(:project => project, :due_date => far_future)
893 Issue.generate!(:project => project, :due_date => far_future)
881 project.versions << Version.generate!(:effective_date => future)
894 project.versions << Version.generate!(:effective_date => future)
882
895
883 assert_equal far_future, project.due_date
896 assert_equal far_future, project.due_date
884 end
897 end
885
898
886 test "#completed_percent with no versions should be 100" do
899 test "#completed_percent with no versions should be 100" do
887 project = Project.generate!
900 project = Project.generate!
888 assert_equal 100, project.completed_percent
901 assert_equal 100, project.completed_percent
889 end
902 end
890
903
891 test "#completed_percent with versions should return 0 if the versions have no issues" do
904 test "#completed_percent with versions should return 0 if the versions have no issues" do
892 project = Project.generate!
905 project = Project.generate!
893 Version.generate!(:project => project)
906 Version.generate!(:project => project)
894 Version.generate!(:project => project)
907 Version.generate!(:project => project)
895
908
896 assert_equal 0, project.completed_percent
909 assert_equal 0, project.completed_percent
897 end
910 end
898
911
899 test "#completed_percent with versions should return 100 if the version has only closed issues" do
912 test "#completed_percent with versions should return 100 if the version has only closed issues" do
900 project = Project.generate!
913 project = Project.generate!
901 project.trackers << Tracker.generate!
914 project.trackers << Tracker.generate!
902 v1 = Version.generate!(:project => project)
915 v1 = Version.generate!(:project => project)
903 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
916 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
904 v2 = Version.generate!(:project => project)
917 v2 = Version.generate!(:project => project)
905 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
918 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
906
919
907 assert_equal 100, project.completed_percent
920 assert_equal 100, project.completed_percent
908 end
921 end
909
922
910 test "#completed_percent with versions should return the averaged completed percent of the versions (not weighted)" do
923 test "#completed_percent with versions should return the averaged completed percent of the versions (not weighted)" do
911 project = Project.generate!
924 project = Project.generate!
912 project.trackers << Tracker.generate!
925 project.trackers << Tracker.generate!
913 v1 = Version.generate!(:project => project)
926 v1 = Version.generate!(:project => project)
914 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
927 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
915 v2 = Version.generate!(:project => project)
928 v2 = Version.generate!(:project => project)
916 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
929 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
917
930
918 assert_equal 50, project.completed_percent
931 assert_equal 50, project.completed_percent
919 end
932 end
920
933
921 test "#notified_users" do
934 test "#notified_users" do
922 project = Project.generate!
935 project = Project.generate!
923 role = Role.generate!
936 role = Role.generate!
924
937
925 user_with_membership_notification = User.generate!(:mail_notification => 'selected')
938 user_with_membership_notification = User.generate!(:mail_notification => 'selected')
926 Member.create!(:project => project, :roles => [role], :principal => user_with_membership_notification, :mail_notification => true)
939 Member.create!(:project => project, :roles => [role], :principal => user_with_membership_notification, :mail_notification => true)
927
940
928 all_events_user = User.generate!(:mail_notification => 'all')
941 all_events_user = User.generate!(:mail_notification => 'all')
929 Member.create!(:project => project, :roles => [role], :principal => all_events_user)
942 Member.create!(:project => project, :roles => [role], :principal => all_events_user)
930
943
931 no_events_user = User.generate!(:mail_notification => 'none')
944 no_events_user = User.generate!(:mail_notification => 'none')
932 Member.create!(:project => project, :roles => [role], :principal => no_events_user)
945 Member.create!(:project => project, :roles => [role], :principal => no_events_user)
933
946
934 only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
947 only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
935 Member.create!(:project => project, :roles => [role], :principal => only_my_events_user)
948 Member.create!(:project => project, :roles => [role], :principal => only_my_events_user)
936
949
937 only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
950 only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
938 Member.create!(:project => project, :roles => [role], :principal => only_assigned_user)
951 Member.create!(:project => project, :roles => [role], :principal => only_assigned_user)
939
952
940 only_owned_user = User.generate!(:mail_notification => 'only_owner')
953 only_owned_user = User.generate!(:mail_notification => 'only_owner')
941 Member.create!(:project => project, :roles => [role], :principal => only_owned_user)
954 Member.create!(:project => project, :roles => [role], :principal => only_owned_user)
942
955
943 assert project.notified_users.include?(user_with_membership_notification), "should include members with a mail notification"
956 assert project.notified_users.include?(user_with_membership_notification), "should include members with a mail notification"
944 assert project.notified_users.include?(all_events_user), "should include users with the 'all' notification option"
957 assert project.notified_users.include?(all_events_user), "should include users with the 'all' notification option"
945 assert !project.notified_users.include?(no_events_user), "should not include users with the 'none' notification option"
958 assert !project.notified_users.include?(no_events_user), "should not include users with the 'none' notification option"
946 assert !project.notified_users.include?(only_my_events_user), "should not include users with the 'only_my_events' notification option"
959 assert !project.notified_users.include?(only_my_events_user), "should not include users with the 'only_my_events' notification option"
947 assert !project.notified_users.include?(only_assigned_user), "should not include users with the 'only_assigned' notification option"
960 assert !project.notified_users.include?(only_assigned_user), "should not include users with the 'only_assigned' notification option"
948 assert !project.notified_users.include?(only_owned_user), "should not include users with the 'only_owner' notification option"
961 assert !project.notified_users.include?(only_owned_user), "should not include users with the 'only_owner' notification option"
949 end
962 end
950
963
951 def test_override_roles_without_builtin_group_memberships
964 def test_override_roles_without_builtin_group_memberships
952 project = Project.generate!
965 project = Project.generate!
953 assert_equal [Role.anonymous], project.override_roles(Role.anonymous)
966 assert_equal [Role.anonymous], project.override_roles(Role.anonymous)
954 assert_equal [Role.non_member], project.override_roles(Role.non_member)
967 assert_equal [Role.non_member], project.override_roles(Role.non_member)
955 end
968 end
956
969
957 def test_css_classes
970 def test_css_classes
958 p = Project.new
971 p = Project.new
959 assert_kind_of String, p.css_classes
972 assert_kind_of String, p.css_classes
960 assert_not_include 'archived', p.css_classes.split
973 assert_not_include 'archived', p.css_classes.split
961 assert_not_include 'closed', p.css_classes.split
974 assert_not_include 'closed', p.css_classes.split
962 end
975 end
963
976
964 def test_css_classes_for_archived_project
977 def test_css_classes_for_archived_project
965 p = Project.new
978 p = Project.new
966 p.status = Project::STATUS_ARCHIVED
979 p.status = Project::STATUS_ARCHIVED
967 assert_include 'archived', p.css_classes.split
980 assert_include 'archived', p.css_classes.split
968 end
981 end
969
982
970 def test_css_classes_for_closed_project
983 def test_css_classes_for_closed_project
971 p = Project.new
984 p = Project.new
972 p.status = Project::STATUS_CLOSED
985 p.status = Project::STATUS_CLOSED
973 assert_include 'closed', p.css_classes.split
986 assert_include 'closed', p.css_classes.split
974 end
987 end
975 end
988 end
General Comments 0
You need to be logged in to leave comments. Login now