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