##// END OF EJS Templates
Select projects with issue_tracking module for gantt display and remove the nil start/due dates trick....
Jean-Philippe Lang -
r4363:b89820080366
parent child
Show More
@@ -1,817 +1,813
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006 Jean-Philippe Lang
2 # Copyright (C) 2006 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 # Project statuses
19 # Project statuses
20 STATUS_ACTIVE = 1
20 STATUS_ACTIVE = 1
21 STATUS_ARCHIVED = 9
21 STATUS_ARCHIVED = 9
22
22
23 # Maximum length for project identifiers
23 # Maximum length for project identifiers
24 IDENTIFIER_MAX_LENGTH = 100
24 IDENTIFIER_MAX_LENGTH = 100
25
25
26 # Specific overidden Activities
26 # Specific overidden Activities
27 has_many :time_entry_activities
27 has_many :time_entry_activities
28 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
28 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}"
29 has_many :memberships, :class_name => 'Member'
29 has_many :memberships, :class_name => 'Member'
30 has_many :member_principals, :class_name => 'Member',
30 has_many :member_principals, :class_name => 'Member',
31 :include => :principal,
31 :include => :principal,
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
32 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})"
33 has_many :users, :through => :members
33 has_many :users, :through => :members
34 has_many :principals, :through => :member_principals, :source => :principal
34 has_many :principals, :through => :member_principals, :source => :principal
35
35
36 has_many :enabled_modules, :dependent => :delete_all
36 has_many :enabled_modules, :dependent => :delete_all
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
37 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
38 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker]
39 has_many :issue_changes, :through => :issues, :source => :journals
39 has_many :issue_changes, :through => :issues, :source => :journals
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
40 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
41 has_many :time_entries, :dependent => :delete_all
41 has_many :time_entries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
42 has_many :queries, :dependent => :delete_all
43 has_many :documents, :dependent => :destroy
43 has_many :documents, :dependent => :destroy
44 has_many :news, :dependent => :delete_all, :include => :author
44 has_many :news, :dependent => :delete_all, :include => :author
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
45 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
46 has_many :boards, :dependent => :destroy, :order => "position ASC"
47 has_one :repository, :dependent => :destroy
47 has_one :repository, :dependent => :destroy
48 has_many :changesets, :through => :repository
48 has_many :changesets, :through => :repository
49 has_one :wiki, :dependent => :destroy
49 has_one :wiki, :dependent => :destroy
50 # Custom field for the project issues
50 # Custom field for the project issues
51 has_and_belongs_to_many :issue_custom_fields,
51 has_and_belongs_to_many :issue_custom_fields,
52 :class_name => 'IssueCustomField',
52 :class_name => 'IssueCustomField',
53 :order => "#{CustomField.table_name}.position",
53 :order => "#{CustomField.table_name}.position",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
54 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
55 :association_foreign_key => 'custom_field_id'
55 :association_foreign_key => 'custom_field_id'
56
56
57 acts_as_nested_set :order => 'name'
57 acts_as_nested_set :order => 'name'
58 acts_as_attachable :view_permission => :view_files,
58 acts_as_attachable :view_permission => :view_files,
59 :delete_permission => :manage_files
59 :delete_permission => :manage_files
60
60
61 acts_as_customizable
61 acts_as_customizable
62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
62 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
63 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
64 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
65 :author => nil
65 :author => nil
66
66
67 attr_protected :status, :enabled_module_names
67 attr_protected :status, :enabled_module_names
68
68
69 validates_presence_of :name, :identifier
69 validates_presence_of :name, :identifier
70 validates_uniqueness_of :identifier
70 validates_uniqueness_of :identifier
71 validates_associated :repository, :wiki
71 validates_associated :repository, :wiki
72 validates_length_of :name, :maximum => 255
72 validates_length_of :name, :maximum => 255
73 validates_length_of :homepage, :maximum => 255
73 validates_length_of :homepage, :maximum => 255
74 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
74 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
75 # donwcase letters, digits, dashes but not digits only
75 # donwcase letters, digits, dashes but not digits only
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
76 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? }
77 # reserved words
77 # reserved words
78 validates_exclusion_of :identifier, :in => %w( new )
78 validates_exclusion_of :identifier, :in => %w( new )
79
79
80 before_destroy :delete_all_members, :destroy_children
80 before_destroy :delete_all_members, :destroy_children
81
81
82 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
82 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } }
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
83 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"}
84 named_scope :all_public, { :conditions => { :is_public => true } }
84 named_scope :all_public, { :conditions => { :is_public => true } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
85 named_scope :visible, lambda { { :conditions => Project.visible_by(User.current) } }
86
86
87 def initialize(attributes = nil)
87 def initialize(attributes = nil)
88 super
88 super
89
89
90 initialized = (attributes || {}).stringify_keys
90 initialized = (attributes || {}).stringify_keys
91 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
91 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
92 self.identifier = Project.next_identifier
92 self.identifier = Project.next_identifier
93 end
93 end
94 if !initialized.key?('is_public')
94 if !initialized.key?('is_public')
95 self.is_public = Setting.default_projects_public?
95 self.is_public = Setting.default_projects_public?
96 end
96 end
97 if !initialized.key?('enabled_module_names')
97 if !initialized.key?('enabled_module_names')
98 self.enabled_module_names = Setting.default_projects_modules
98 self.enabled_module_names = Setting.default_projects_modules
99 end
99 end
100 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
100 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
101 self.trackers = Tracker.all
101 self.trackers = Tracker.all
102 end
102 end
103 end
103 end
104
104
105 def identifier=(identifier)
105 def identifier=(identifier)
106 super unless identifier_frozen?
106 super unless identifier_frozen?
107 end
107 end
108
108
109 def identifier_frozen?
109 def identifier_frozen?
110 errors[:identifier].nil? && !(new_record? || identifier.blank?)
110 errors[:identifier].nil? && !(new_record? || identifier.blank?)
111 end
111 end
112
112
113 # returns latest created projects
113 # returns latest created projects
114 # non public projects will be returned only if user is a member of those
114 # non public projects will be returned only if user is a member of those
115 def self.latest(user=nil, count=5)
115 def self.latest(user=nil, count=5)
116 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
116 find(:all, :limit => count, :conditions => visible_by(user), :order => "created_on DESC")
117 end
117 end
118
118
119 # Returns a SQL :conditions string used to find all active projects for the specified user.
119 # Returns a SQL :conditions string used to find all active projects for the specified user.
120 #
120 #
121 # Examples:
121 # Examples:
122 # Projects.visible_by(admin) => "projects.status = 1"
122 # Projects.visible_by(admin) => "projects.status = 1"
123 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
123 # Projects.visible_by(normal_user) => "projects.status = 1 AND projects.is_public = 1"
124 def self.visible_by(user=nil)
124 def self.visible_by(user=nil)
125 user ||= User.current
125 user ||= User.current
126 if user && user.admin?
126 if user && user.admin?
127 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
127 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
128 elsif user && user.memberships.any?
128 elsif user && user.memberships.any?
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
129 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND (#{Project.table_name}.is_public = #{connection.quoted_true} or #{Project.table_name}.id IN (#{user.memberships.collect{|m| m.project_id}.join(',')}))"
130 else
130 else
131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
131 return "#{Project.table_name}.status=#{Project::STATUS_ACTIVE} AND #{Project.table_name}.is_public = #{connection.quoted_true}"
132 end
132 end
133 end
133 end
134
134
135 def self.allowed_to_condition(user, permission, options={})
135 def self.allowed_to_condition(user, permission, options={})
136 statements = []
136 statements = []
137 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
137 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"
138 if perm = Redmine::AccessControl.permission(permission)
138 if perm = Redmine::AccessControl.permission(permission)
139 unless perm.project_module.nil?
139 unless perm.project_module.nil?
140 # If the permission belongs to a project module, make sure the module is enabled
140 # If the permission belongs to a project module, make sure the module is enabled
141 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
141 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
142 end
142 end
143 end
143 end
144 if options[:project]
144 if options[:project]
145 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
145 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
146 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
146 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
147 base_statement = "(#{project_statement}) AND (#{base_statement})"
147 base_statement = "(#{project_statement}) AND (#{base_statement})"
148 end
148 end
149 if user.admin?
149 if user.admin?
150 # no restriction
150 # no restriction
151 else
151 else
152 statements << "1=0"
152 statements << "1=0"
153 if user.logged?
153 if user.logged?
154 if Role.non_member.allowed_to?(permission) && !options[:member]
154 if Role.non_member.allowed_to?(permission) && !options[:member]
155 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
155 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
156 end
156 end
157 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
157 allowed_project_ids = user.memberships.select {|m| m.roles.detect {|role| role.allowed_to?(permission)}}.collect {|m| m.project_id}
158 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
158 statements << "#{Project.table_name}.id IN (#{allowed_project_ids.join(',')})" if allowed_project_ids.any?
159 else
159 else
160 if Role.anonymous.allowed_to?(permission) && !options[:member]
160 if Role.anonymous.allowed_to?(permission) && !options[:member]
161 # anonymous user allowed on public project
161 # anonymous user allowed on public project
162 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
162 statements << "#{Project.table_name}.is_public = #{connection.quoted_true}"
163 end
163 end
164 end
164 end
165 end
165 end
166 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
166 statements.empty? ? base_statement : "((#{base_statement}) AND (#{statements.join(' OR ')}))"
167 end
167 end
168
168
169 # Returns the Systemwide and project specific activities
169 # Returns the Systemwide and project specific activities
170 def activities(include_inactive=false)
170 def activities(include_inactive=false)
171 if include_inactive
171 if include_inactive
172 return all_activities
172 return all_activities
173 else
173 else
174 return active_activities
174 return active_activities
175 end
175 end
176 end
176 end
177
177
178 # Will create a new Project specific Activity or update an existing one
178 # Will create a new Project specific Activity or update an existing one
179 #
179 #
180 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
180 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
181 # does not successfully save.
181 # does not successfully save.
182 def update_or_create_time_entry_activity(id, activity_hash)
182 def update_or_create_time_entry_activity(id, activity_hash)
183 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
183 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
184 self.create_time_entry_activity_if_needed(activity_hash)
184 self.create_time_entry_activity_if_needed(activity_hash)
185 else
185 else
186 activity = project.time_entry_activities.find_by_id(id.to_i)
186 activity = project.time_entry_activities.find_by_id(id.to_i)
187 activity.update_attributes(activity_hash) if activity
187 activity.update_attributes(activity_hash) if activity
188 end
188 end
189 end
189 end
190
190
191 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
191 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
192 #
192 #
193 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
193 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
194 # does not successfully save.
194 # does not successfully save.
195 def create_time_entry_activity_if_needed(activity)
195 def create_time_entry_activity_if_needed(activity)
196 if activity['parent_id']
196 if activity['parent_id']
197
197
198 parent_activity = TimeEntryActivity.find(activity['parent_id'])
198 parent_activity = TimeEntryActivity.find(activity['parent_id'])
199 activity['name'] = parent_activity.name
199 activity['name'] = parent_activity.name
200 activity['position'] = parent_activity.position
200 activity['position'] = parent_activity.position
201
201
202 if Enumeration.overridding_change?(activity, parent_activity)
202 if Enumeration.overridding_change?(activity, parent_activity)
203 project_activity = self.time_entry_activities.create(activity)
203 project_activity = self.time_entry_activities.create(activity)
204
204
205 if project_activity.new_record?
205 if project_activity.new_record?
206 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
206 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
207 else
207 else
208 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
208 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
209 end
209 end
210 end
210 end
211 end
211 end
212 end
212 end
213
213
214 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
214 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
215 #
215 #
216 # Examples:
216 # Examples:
217 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
217 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
218 # project.project_condition(false) => "projects.id = 1"
218 # project.project_condition(false) => "projects.id = 1"
219 def project_condition(with_subprojects)
219 def project_condition(with_subprojects)
220 cond = "#{Project.table_name}.id = #{id}"
220 cond = "#{Project.table_name}.id = #{id}"
221 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
221 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
222 cond
222 cond
223 end
223 end
224
224
225 def self.find(*args)
225 def self.find(*args)
226 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
226 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
227 project = find_by_identifier(*args)
227 project = find_by_identifier(*args)
228 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
228 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
229 project
229 project
230 else
230 else
231 super
231 super
232 end
232 end
233 end
233 end
234
234
235 def to_param
235 def to_param
236 # id is used for projects with a numeric identifier (compatibility)
236 # id is used for projects with a numeric identifier (compatibility)
237 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
237 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id : identifier)
238 end
238 end
239
239
240 def active?
240 def active?
241 self.status == STATUS_ACTIVE
241 self.status == STATUS_ACTIVE
242 end
242 end
243
243
244 def archived?
244 def archived?
245 self.status == STATUS_ARCHIVED
245 self.status == STATUS_ARCHIVED
246 end
246 end
247
247
248 # Archives the project and its descendants
248 # Archives the project and its descendants
249 def archive
249 def archive
250 # Check that there is no issue of a non descendant project that is assigned
250 # Check that there is no issue of a non descendant project that is assigned
251 # to one of the project or descendant versions
251 # to one of the project or descendant versions
252 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
252 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
253 if v_ids.any? && Issue.find(:first, :include => :project,
253 if v_ids.any? && Issue.find(:first, :include => :project,
254 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
254 :conditions => ["(#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?)" +
255 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
255 " AND #{Issue.table_name}.fixed_version_id IN (?)", lft, rgt, v_ids])
256 return false
256 return false
257 end
257 end
258 Project.transaction do
258 Project.transaction do
259 archive!
259 archive!
260 end
260 end
261 true
261 true
262 end
262 end
263
263
264 # Unarchives the project
264 # Unarchives the project
265 # All its ancestors must be active
265 # All its ancestors must be active
266 def unarchive
266 def unarchive
267 return false if ancestors.detect {|a| !a.active?}
267 return false if ancestors.detect {|a| !a.active?}
268 update_attribute :status, STATUS_ACTIVE
268 update_attribute :status, STATUS_ACTIVE
269 end
269 end
270
270
271 # Returns an array of projects the project can be moved to
271 # Returns an array of projects the project can be moved to
272 # by the current user
272 # by the current user
273 def allowed_parents
273 def allowed_parents
274 return @allowed_parents if @allowed_parents
274 return @allowed_parents if @allowed_parents
275 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
275 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects))
276 @allowed_parents = @allowed_parents - self_and_descendants
276 @allowed_parents = @allowed_parents - self_and_descendants
277 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
277 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
278 @allowed_parents << nil
278 @allowed_parents << nil
279 end
279 end
280 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
280 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
281 @allowed_parents << parent
281 @allowed_parents << parent
282 end
282 end
283 @allowed_parents
283 @allowed_parents
284 end
284 end
285
285
286 # Sets the parent of the project with authorization check
286 # Sets the parent of the project with authorization check
287 def set_allowed_parent!(p)
287 def set_allowed_parent!(p)
288 unless p.nil? || p.is_a?(Project)
288 unless p.nil? || p.is_a?(Project)
289 if p.to_s.blank?
289 if p.to_s.blank?
290 p = nil
290 p = nil
291 else
291 else
292 p = Project.find_by_id(p)
292 p = Project.find_by_id(p)
293 return false unless p
293 return false unless p
294 end
294 end
295 end
295 end
296 if p.nil?
296 if p.nil?
297 if !new_record? && allowed_parents.empty?
297 if !new_record? && allowed_parents.empty?
298 return false
298 return false
299 end
299 end
300 elsif !allowed_parents.include?(p)
300 elsif !allowed_parents.include?(p)
301 return false
301 return false
302 end
302 end
303 set_parent!(p)
303 set_parent!(p)
304 end
304 end
305
305
306 # Sets the parent of the project
306 # Sets the parent of the project
307 # Argument can be either a Project, a String, a Fixnum or nil
307 # Argument can be either a Project, a String, a Fixnum or nil
308 def set_parent!(p)
308 def set_parent!(p)
309 unless p.nil? || p.is_a?(Project)
309 unless p.nil? || p.is_a?(Project)
310 if p.to_s.blank?
310 if p.to_s.blank?
311 p = nil
311 p = nil
312 else
312 else
313 p = Project.find_by_id(p)
313 p = Project.find_by_id(p)
314 return false unless p
314 return false unless p
315 end
315 end
316 end
316 end
317 if p == parent && !p.nil?
317 if p == parent && !p.nil?
318 # Nothing to do
318 # Nothing to do
319 true
319 true
320 elsif p.nil? || (p.active? && move_possible?(p))
320 elsif p.nil? || (p.active? && move_possible?(p))
321 # Insert the project so that target's children or root projects stay alphabetically sorted
321 # Insert the project so that target's children or root projects stay alphabetically sorted
322 sibs = (p.nil? ? self.class.roots : p.children)
322 sibs = (p.nil? ? self.class.roots : p.children)
323 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
323 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
324 if to_be_inserted_before
324 if to_be_inserted_before
325 move_to_left_of(to_be_inserted_before)
325 move_to_left_of(to_be_inserted_before)
326 elsif p.nil?
326 elsif p.nil?
327 if sibs.empty?
327 if sibs.empty?
328 # move_to_root adds the project in first (ie. left) position
328 # move_to_root adds the project in first (ie. left) position
329 move_to_root
329 move_to_root
330 else
330 else
331 move_to_right_of(sibs.last) unless self == sibs.last
331 move_to_right_of(sibs.last) unless self == sibs.last
332 end
332 end
333 else
333 else
334 # move_to_child_of adds the project in last (ie.right) position
334 # move_to_child_of adds the project in last (ie.right) position
335 move_to_child_of(p)
335 move_to_child_of(p)
336 end
336 end
337 Issue.update_versions_from_hierarchy_change(self)
337 Issue.update_versions_from_hierarchy_change(self)
338 true
338 true
339 else
339 else
340 # Can not move to the given target
340 # Can not move to the given target
341 false
341 false
342 end
342 end
343 end
343 end
344
344
345 # Returns an array of the trackers used by the project and its active sub projects
345 # Returns an array of the trackers used by the project and its active sub projects
346 def rolled_up_trackers
346 def rolled_up_trackers
347 @rolled_up_trackers ||=
347 @rolled_up_trackers ||=
348 Tracker.find(:all, :include => :projects,
348 Tracker.find(:all, :include => :projects,
349 :select => "DISTINCT #{Tracker.table_name}.*",
349 :select => "DISTINCT #{Tracker.table_name}.*",
350 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
350 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt],
351 :order => "#{Tracker.table_name}.position")
351 :order => "#{Tracker.table_name}.position")
352 end
352 end
353
353
354 # Closes open and locked project versions that are completed
354 # Closes open and locked project versions that are completed
355 def close_completed_versions
355 def close_completed_versions
356 Version.transaction do
356 Version.transaction do
357 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
357 versions.find(:all, :conditions => {:status => %w(open locked)}).each do |version|
358 if version.completed?
358 if version.completed?
359 version.update_attribute(:status, 'closed')
359 version.update_attribute(:status, 'closed')
360 end
360 end
361 end
361 end
362 end
362 end
363 end
363 end
364
364
365 # Returns a scope of the Versions on subprojects
365 # Returns a scope of the Versions on subprojects
366 def rolled_up_versions
366 def rolled_up_versions
367 @rolled_up_versions ||=
367 @rolled_up_versions ||=
368 Version.scoped(:include => :project,
368 Version.scoped(:include => :project,
369 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
369 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt])
370 end
370 end
371
371
372 # Returns a scope of the Versions used by the project
372 # Returns a scope of the Versions used by the project
373 def shared_versions
373 def shared_versions
374 @shared_versions ||=
374 @shared_versions ||=
375 Version.scoped(:include => :project,
375 Version.scoped(:include => :project,
376 :conditions => "#{Project.table_name}.id = #{id}" +
376 :conditions => "#{Project.table_name}.id = #{id}" +
377 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
377 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" +
378 " #{Version.table_name}.sharing = 'system'" +
378 " #{Version.table_name}.sharing = 'system'" +
379 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
379 " OR (#{Project.table_name}.lft >= #{root.lft} AND #{Project.table_name}.rgt <= #{root.rgt} AND #{Version.table_name}.sharing = 'tree')" +
380 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
380 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
381 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
381 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
382 "))")
382 "))")
383 end
383 end
384
384
385 # Returns a hash of project users grouped by role
385 # Returns a hash of project users grouped by role
386 def users_by_role
386 def users_by_role
387 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
387 members.find(:all, :include => [:user, :roles]).inject({}) do |h, m|
388 m.roles.each do |r|
388 m.roles.each do |r|
389 h[r] ||= []
389 h[r] ||= []
390 h[r] << m.user
390 h[r] << m.user
391 end
391 end
392 h
392 h
393 end
393 end
394 end
394 end
395
395
396 # Deletes all project's members
396 # Deletes all project's members
397 def delete_all_members
397 def delete_all_members
398 me, mr = Member.table_name, MemberRole.table_name
398 me, mr = Member.table_name, MemberRole.table_name
399 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
399 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
400 Member.delete_all(['project_id = ?', id])
400 Member.delete_all(['project_id = ?', id])
401 end
401 end
402
402
403 # Users issues can be assigned to
403 # Users issues can be assigned to
404 def assignable_users
404 def assignable_users
405 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
405 members.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.user}.sort
406 end
406 end
407
407
408 # Returns the mail adresses of users that should be always notified on project events
408 # Returns the mail adresses of users that should be always notified on project events
409 def recipients
409 def recipients
410 notified_users.collect {|user| user.mail}
410 notified_users.collect {|user| user.mail}
411 end
411 end
412
412
413 # Returns the users that should be notified on project events
413 # Returns the users that should be notified on project events
414 def notified_users
414 def notified_users
415 # TODO: User part should be extracted to User#notify_about?
415 # TODO: User part should be extracted to User#notify_about?
416 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
416 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user}
417 end
417 end
418
418
419 # Returns an array of all custom fields enabled for project issues
419 # Returns an array of all custom fields enabled for project issues
420 # (explictly associated custom fields and custom fields enabled for all projects)
420 # (explictly associated custom fields and custom fields enabled for all projects)
421 def all_issue_custom_fields
421 def all_issue_custom_fields
422 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
422 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
423 end
423 end
424
424
425 def project
425 def project
426 self
426 self
427 end
427 end
428
428
429 def <=>(project)
429 def <=>(project)
430 name.downcase <=> project.name.downcase
430 name.downcase <=> project.name.downcase
431 end
431 end
432
432
433 def to_s
433 def to_s
434 name
434 name
435 end
435 end
436
436
437 # Returns a short description of the projects (first lines)
437 # Returns a short description of the projects (first lines)
438 def short_description(length = 255)
438 def short_description(length = 255)
439 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
439 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
440 end
440 end
441
441
442 def css_classes
442 def css_classes
443 s = 'project'
443 s = 'project'
444 s << ' root' if root?
444 s << ' root' if root?
445 s << ' child' if child?
445 s << ' child' if child?
446 s << (leaf? ? ' leaf' : ' parent')
446 s << (leaf? ? ' leaf' : ' parent')
447 s
447 s
448 end
448 end
449
449
450 # The earliest start date of a project, based on it's issues and versions
450 # The earliest start date of a project, based on it's issues and versions
451 def start_date
451 def start_date
452 if module_enabled?(:issue_tracking)
452 [
453 [
453 issues.minimum('start_date'),
454 issues.minimum('start_date'),
454 shared_versions.collect(&:effective_date),
455 shared_versions.collect(&:effective_date),
455 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
456 shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
456 ].flatten.compact.min
457 ].flatten.compact.min
458 end
459 end
457 end
460
458
461 # The latest due date of an issue or version
459 # The latest due date of an issue or version
462 def due_date
460 def due_date
463 if module_enabled?(:issue_tracking)
461 [
464 [
462 issues.maximum('due_date'),
465 issues.maximum('due_date'),
463 shared_versions.collect(&:effective_date),
466 shared_versions.collect(&:effective_date),
464 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
467 shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
465 ].flatten.compact.max
468 ].flatten.compact.max
469 end
470 end
466 end
471
467
472 def overdue?
468 def overdue?
473 active? && !due_date.nil? && (due_date < Date.today)
469 active? && !due_date.nil? && (due_date < Date.today)
474 end
470 end
475
471
476 # Returns the percent completed for this project, based on the
472 # Returns the percent completed for this project, based on the
477 # progress on it's versions.
473 # progress on it's versions.
478 def completed_percent(options={:include_subprojects => false})
474 def completed_percent(options={:include_subprojects => false})
479 if options.delete(:include_subprojects)
475 if options.delete(:include_subprojects)
480 total = self_and_descendants.collect(&:completed_percent).sum
476 total = self_and_descendants.collect(&:completed_percent).sum
481
477
482 total / self_and_descendants.count
478 total / self_and_descendants.count
483 else
479 else
484 if versions.count > 0
480 if versions.count > 0
485 total = versions.collect(&:completed_pourcent).sum
481 total = versions.collect(&:completed_pourcent).sum
486
482
487 total / versions.count
483 total / versions.count
488 else
484 else
489 100
485 100
490 end
486 end
491 end
487 end
492 end
488 end
493
489
494 # Return true if this project is allowed to do the specified action.
490 # Return true if this project is allowed to do the specified action.
495 # action can be:
491 # action can be:
496 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
492 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
497 # * a permission Symbol (eg. :edit_project)
493 # * a permission Symbol (eg. :edit_project)
498 def allows_to?(action)
494 def allows_to?(action)
499 if action.is_a? Hash
495 if action.is_a? Hash
500 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
496 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
501 else
497 else
502 allowed_permissions.include? action
498 allowed_permissions.include? action
503 end
499 end
504 end
500 end
505
501
506 def module_enabled?(module_name)
502 def module_enabled?(module_name)
507 module_name = module_name.to_s
503 module_name = module_name.to_s
508 enabled_modules.detect {|m| m.name == module_name}
504 enabled_modules.detect {|m| m.name == module_name}
509 end
505 end
510
506
511 def enabled_module_names=(module_names)
507 def enabled_module_names=(module_names)
512 if module_names && module_names.is_a?(Array)
508 if module_names && module_names.is_a?(Array)
513 module_names = module_names.collect(&:to_s).reject(&:blank?)
509 module_names = module_names.collect(&:to_s).reject(&:blank?)
514 # remove disabled modules
510 # remove disabled modules
515 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
511 enabled_modules.each {|mod| mod.destroy unless module_names.include?(mod.name)}
516 # add new modules
512 # add new modules
517 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
513 module_names.reject {|name| module_enabled?(name)}.each {|name| enabled_modules << EnabledModule.new(:name => name)}
518 else
514 else
519 enabled_modules.clear
515 enabled_modules.clear
520 end
516 end
521 end
517 end
522
518
523 # Returns an array of the enabled modules names
519 # Returns an array of the enabled modules names
524 def enabled_module_names
520 def enabled_module_names
525 enabled_modules.collect(&:name)
521 enabled_modules.collect(&:name)
526 end
522 end
527
523
528 # Returns an array of projects that are in this project's hierarchy
524 # Returns an array of projects that are in this project's hierarchy
529 #
525 #
530 # Example: parents, children, siblings
526 # Example: parents, children, siblings
531 def hierarchy
527 def hierarchy
532 parents = project.self_and_ancestors || []
528 parents = project.self_and_ancestors || []
533 descendants = project.descendants || []
529 descendants = project.descendants || []
534 project_hierarchy = parents | descendants # Set union
530 project_hierarchy = parents | descendants # Set union
535 end
531 end
536
532
537 # Returns an auto-generated project identifier based on the last identifier used
533 # Returns an auto-generated project identifier based on the last identifier used
538 def self.next_identifier
534 def self.next_identifier
539 p = Project.find(:first, :order => 'created_on DESC')
535 p = Project.find(:first, :order => 'created_on DESC')
540 p.nil? ? nil : p.identifier.to_s.succ
536 p.nil? ? nil : p.identifier.to_s.succ
541 end
537 end
542
538
543 # Copies and saves the Project instance based on the +project+.
539 # Copies and saves the Project instance based on the +project+.
544 # Duplicates the source project's:
540 # Duplicates the source project's:
545 # * Wiki
541 # * Wiki
546 # * Versions
542 # * Versions
547 # * Categories
543 # * Categories
548 # * Issues
544 # * Issues
549 # * Members
545 # * Members
550 # * Queries
546 # * Queries
551 #
547 #
552 # Accepts an +options+ argument to specify what to copy
548 # Accepts an +options+ argument to specify what to copy
553 #
549 #
554 # Examples:
550 # Examples:
555 # project.copy(1) # => copies everything
551 # project.copy(1) # => copies everything
556 # project.copy(1, :only => 'members') # => copies members only
552 # project.copy(1, :only => 'members') # => copies members only
557 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
553 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
558 def copy(project, options={})
554 def copy(project, options={})
559 project = project.is_a?(Project) ? project : Project.find(project)
555 project = project.is_a?(Project) ? project : Project.find(project)
560
556
561 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
557 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
562 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
558 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
563
559
564 Project.transaction do
560 Project.transaction do
565 if save
561 if save
566 reload
562 reload
567 to_be_copied.each do |name|
563 to_be_copied.each do |name|
568 send "copy_#{name}", project
564 send "copy_#{name}", project
569 end
565 end
570 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
566 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
571 save
567 save
572 end
568 end
573 end
569 end
574 end
570 end
575
571
576
572
577 # Copies +project+ and returns the new instance. This will not save
573 # Copies +project+ and returns the new instance. This will not save
578 # the copy
574 # the copy
579 def self.copy_from(project)
575 def self.copy_from(project)
580 begin
576 begin
581 project = project.is_a?(Project) ? project : Project.find(project)
577 project = project.is_a?(Project) ? project : Project.find(project)
582 if project
578 if project
583 # clear unique attributes
579 # clear unique attributes
584 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
580 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
585 copy = Project.new(attributes)
581 copy = Project.new(attributes)
586 copy.enabled_modules = project.enabled_modules
582 copy.enabled_modules = project.enabled_modules
587 copy.trackers = project.trackers
583 copy.trackers = project.trackers
588 copy.custom_values = project.custom_values.collect {|v| v.clone}
584 copy.custom_values = project.custom_values.collect {|v| v.clone}
589 copy.issue_custom_fields = project.issue_custom_fields
585 copy.issue_custom_fields = project.issue_custom_fields
590 return copy
586 return copy
591 else
587 else
592 return nil
588 return nil
593 end
589 end
594 rescue ActiveRecord::RecordNotFound
590 rescue ActiveRecord::RecordNotFound
595 return nil
591 return nil
596 end
592 end
597 end
593 end
598
594
599 # Yields the given block for each project with its level in the tree
595 # Yields the given block for each project with its level in the tree
600 def self.project_tree(projects, &block)
596 def self.project_tree(projects, &block)
601 ancestors = []
597 ancestors = []
602 projects.sort_by(&:lft).each do |project|
598 projects.sort_by(&:lft).each do |project|
603 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
599 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
604 ancestors.pop
600 ancestors.pop
605 end
601 end
606 yield project, ancestors.size
602 yield project, ancestors.size
607 ancestors << project
603 ancestors << project
608 end
604 end
609 end
605 end
610
606
611 private
607 private
612
608
613 # Destroys children before destroying self
609 # Destroys children before destroying self
614 def destroy_children
610 def destroy_children
615 children.each do |child|
611 children.each do |child|
616 child.destroy
612 child.destroy
617 end
613 end
618 end
614 end
619
615
620 # Copies wiki from +project+
616 # Copies wiki from +project+
621 def copy_wiki(project)
617 def copy_wiki(project)
622 # Check that the source project has a wiki first
618 # Check that the source project has a wiki first
623 unless project.wiki.nil?
619 unless project.wiki.nil?
624 self.wiki ||= Wiki.new
620 self.wiki ||= Wiki.new
625 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
621 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
626 wiki_pages_map = {}
622 wiki_pages_map = {}
627 project.wiki.pages.each do |page|
623 project.wiki.pages.each do |page|
628 # Skip pages without content
624 # Skip pages without content
629 next if page.content.nil?
625 next if page.content.nil?
630 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
626 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
631 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
627 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
632 new_wiki_page.content = new_wiki_content
628 new_wiki_page.content = new_wiki_content
633 wiki.pages << new_wiki_page
629 wiki.pages << new_wiki_page
634 wiki_pages_map[page.id] = new_wiki_page
630 wiki_pages_map[page.id] = new_wiki_page
635 end
631 end
636 wiki.save
632 wiki.save
637 # Reproduce page hierarchy
633 # Reproduce page hierarchy
638 project.wiki.pages.each do |page|
634 project.wiki.pages.each do |page|
639 if page.parent_id && wiki_pages_map[page.id]
635 if page.parent_id && wiki_pages_map[page.id]
640 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
636 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
641 wiki_pages_map[page.id].save
637 wiki_pages_map[page.id].save
642 end
638 end
643 end
639 end
644 end
640 end
645 end
641 end
646
642
647 # Copies versions from +project+
643 # Copies versions from +project+
648 def copy_versions(project)
644 def copy_versions(project)
649 project.versions.each do |version|
645 project.versions.each do |version|
650 new_version = Version.new
646 new_version = Version.new
651 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
647 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
652 self.versions << new_version
648 self.versions << new_version
653 end
649 end
654 end
650 end
655
651
656 # Copies issue categories from +project+
652 # Copies issue categories from +project+
657 def copy_issue_categories(project)
653 def copy_issue_categories(project)
658 project.issue_categories.each do |issue_category|
654 project.issue_categories.each do |issue_category|
659 new_issue_category = IssueCategory.new
655 new_issue_category = IssueCategory.new
660 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
656 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
661 self.issue_categories << new_issue_category
657 self.issue_categories << new_issue_category
662 end
658 end
663 end
659 end
664
660
665 # Copies issues from +project+
661 # Copies issues from +project+
666 def copy_issues(project)
662 def copy_issues(project)
667 # Stores the source issue id as a key and the copied issues as the
663 # Stores the source issue id as a key and the copied issues as the
668 # value. Used to map the two togeather for issue relations.
664 # value. Used to map the two togeather for issue relations.
669 issues_map = {}
665 issues_map = {}
670
666
671 # Get issues sorted by root_id, lft so that parent issues
667 # Get issues sorted by root_id, lft so that parent issues
672 # get copied before their children
668 # get copied before their children
673 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
669 project.issues.find(:all, :order => 'root_id, lft').each do |issue|
674 new_issue = Issue.new
670 new_issue = Issue.new
675 new_issue.copy_from(issue)
671 new_issue.copy_from(issue)
676 new_issue.project = self
672 new_issue.project = self
677 # Reassign fixed_versions by name, since names are unique per
673 # Reassign fixed_versions by name, since names are unique per
678 # project and the versions for self are not yet saved
674 # project and the versions for self are not yet saved
679 if issue.fixed_version
675 if issue.fixed_version
680 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
676 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first
681 end
677 end
682 # Reassign the category by name, since names are unique per
678 # Reassign the category by name, since names are unique per
683 # project and the categories for self are not yet saved
679 # project and the categories for self are not yet saved
684 if issue.category
680 if issue.category
685 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
681 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first
686 end
682 end
687 # Parent issue
683 # Parent issue
688 if issue.parent_id
684 if issue.parent_id
689 if copied_parent = issues_map[issue.parent_id]
685 if copied_parent = issues_map[issue.parent_id]
690 new_issue.parent_issue_id = copied_parent.id
686 new_issue.parent_issue_id = copied_parent.id
691 end
687 end
692 end
688 end
693
689
694 self.issues << new_issue
690 self.issues << new_issue
695 issues_map[issue.id] = new_issue
691 issues_map[issue.id] = new_issue
696 end
692 end
697
693
698 # Relations after in case issues related each other
694 # Relations after in case issues related each other
699 project.issues.each do |issue|
695 project.issues.each do |issue|
700 new_issue = issues_map[issue.id]
696 new_issue = issues_map[issue.id]
701
697
702 # Relations
698 # Relations
703 issue.relations_from.each do |source_relation|
699 issue.relations_from.each do |source_relation|
704 new_issue_relation = IssueRelation.new
700 new_issue_relation = IssueRelation.new
705 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
701 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
706 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
702 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
707 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
703 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
708 new_issue_relation.issue_to = source_relation.issue_to
704 new_issue_relation.issue_to = source_relation.issue_to
709 end
705 end
710 new_issue.relations_from << new_issue_relation
706 new_issue.relations_from << new_issue_relation
711 end
707 end
712
708
713 issue.relations_to.each do |source_relation|
709 issue.relations_to.each do |source_relation|
714 new_issue_relation = IssueRelation.new
710 new_issue_relation = IssueRelation.new
715 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
711 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
716 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
712 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
717 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
713 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
718 new_issue_relation.issue_from = source_relation.issue_from
714 new_issue_relation.issue_from = source_relation.issue_from
719 end
715 end
720 new_issue.relations_to << new_issue_relation
716 new_issue.relations_to << new_issue_relation
721 end
717 end
722 end
718 end
723 end
719 end
724
720
725 # Copies members from +project+
721 # Copies members from +project+
726 def copy_members(project)
722 def copy_members(project)
727 project.memberships.each do |member|
723 project.memberships.each do |member|
728 new_member = Member.new
724 new_member = Member.new
729 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
725 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
730 # only copy non inherited roles
726 # only copy non inherited roles
731 # inherited roles will be added when copying the group membership
727 # inherited roles will be added when copying the group membership
732 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
728 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
733 next if role_ids.empty?
729 next if role_ids.empty?
734 new_member.role_ids = role_ids
730 new_member.role_ids = role_ids
735 new_member.project = self
731 new_member.project = self
736 self.members << new_member
732 self.members << new_member
737 end
733 end
738 end
734 end
739
735
740 # Copies queries from +project+
736 # Copies queries from +project+
741 def copy_queries(project)
737 def copy_queries(project)
742 project.queries.each do |query|
738 project.queries.each do |query|
743 new_query = Query.new
739 new_query = Query.new
744 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
740 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
745 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
741 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
746 new_query.project = self
742 new_query.project = self
747 self.queries << new_query
743 self.queries << new_query
748 end
744 end
749 end
745 end
750
746
751 # Copies boards from +project+
747 # Copies boards from +project+
752 def copy_boards(project)
748 def copy_boards(project)
753 project.boards.each do |board|
749 project.boards.each do |board|
754 new_board = Board.new
750 new_board = Board.new
755 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
751 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
756 new_board.project = self
752 new_board.project = self
757 self.boards << new_board
753 self.boards << new_board
758 end
754 end
759 end
755 end
760
756
761 def allowed_permissions
757 def allowed_permissions
762 @allowed_permissions ||= begin
758 @allowed_permissions ||= begin
763 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
759 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
764 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
760 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
765 end
761 end
766 end
762 end
767
763
768 def allowed_actions
764 def allowed_actions
769 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
765 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
770 end
766 end
771
767
772 # Returns all the active Systemwide and project specific activities
768 # Returns all the active Systemwide and project specific activities
773 def active_activities
769 def active_activities
774 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
770 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
775
771
776 if overridden_activity_ids.empty?
772 if overridden_activity_ids.empty?
777 return TimeEntryActivity.shared.active
773 return TimeEntryActivity.shared.active
778 else
774 else
779 return system_activities_and_project_overrides
775 return system_activities_and_project_overrides
780 end
776 end
781 end
777 end
782
778
783 # Returns all the Systemwide and project specific activities
779 # Returns all the Systemwide and project specific activities
784 # (inactive and active)
780 # (inactive and active)
785 def all_activities
781 def all_activities
786 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
782 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
787
783
788 if overridden_activity_ids.empty?
784 if overridden_activity_ids.empty?
789 return TimeEntryActivity.shared
785 return TimeEntryActivity.shared
790 else
786 else
791 return system_activities_and_project_overrides(true)
787 return system_activities_and_project_overrides(true)
792 end
788 end
793 end
789 end
794
790
795 # Returns the systemwide active activities merged with the project specific overrides
791 # Returns the systemwide active activities merged with the project specific overrides
796 def system_activities_and_project_overrides(include_inactive=false)
792 def system_activities_and_project_overrides(include_inactive=false)
797 if include_inactive
793 if include_inactive
798 return TimeEntryActivity.shared.
794 return TimeEntryActivity.shared.
799 find(:all,
795 find(:all,
800 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
796 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
801 self.time_entry_activities
797 self.time_entry_activities
802 else
798 else
803 return TimeEntryActivity.shared.active.
799 return TimeEntryActivity.shared.active.
804 find(:all,
800 find(:all,
805 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
801 :conditions => ["id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)]) +
806 self.time_entry_activities.active
802 self.time_entry_activities.active
807 end
803 end
808 end
804 end
809
805
810 # Archives subprojects recursively
806 # Archives subprojects recursively
811 def archive!
807 def archive!
812 children.each do |subproject|
808 children.each do |subproject|
813 subproject.send :archive!
809 subproject.send :archive!
814 end
810 end
815 update_attribute :status, STATUS_ARCHIVED
811 update_attribute :status, STATUS_ARCHIVED
816 end
812 end
817 end
813 end
@@ -1,974 +1,974
1 # Redmine - project management software
1 # Redmine - project management software
2 # Copyright (C) 2006-2008 Jean-Philippe Lang
2 # Copyright (C) 2006-2008 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 module Redmine
18 module Redmine
19 module Helpers
19 module Helpers
20 # Simple class to handle gantt chart data
20 # Simple class to handle gantt chart data
21 class Gantt
21 class Gantt
22 include ERB::Util
22 include ERB::Util
23 include Redmine::I18n
23 include Redmine::I18n
24
24
25 # :nodoc:
25 # :nodoc:
26 # Some utility methods for the PDF export
26 # Some utility methods for the PDF export
27 class PDF
27 class PDF
28 MaxCharactorsForSubject = 45
28 MaxCharactorsForSubject = 45
29 TotalWidth = 280
29 TotalWidth = 280
30 LeftPaneWidth = 100
30 LeftPaneWidth = 100
31
31
32 def self.right_pane_width
32 def self.right_pane_width
33 TotalWidth - LeftPaneWidth
33 TotalWidth - LeftPaneWidth
34 end
34 end
35 end
35 end
36
36
37 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months
37 attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months
38 attr_accessor :query
38 attr_accessor :query
39 attr_accessor :project
39 attr_accessor :project
40 attr_accessor :view
40 attr_accessor :view
41
41
42 def initialize(options={})
42 def initialize(options={})
43 options = options.dup
43 options = options.dup
44
44
45 if options[:year] && options[:year].to_i >0
45 if options[:year] && options[:year].to_i >0
46 @year_from = options[:year].to_i
46 @year_from = options[:year].to_i
47 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
47 if options[:month] && options[:month].to_i >=1 && options[:month].to_i <= 12
48 @month_from = options[:month].to_i
48 @month_from = options[:month].to_i
49 else
49 else
50 @month_from = 1
50 @month_from = 1
51 end
51 end
52 else
52 else
53 @month_from ||= Date.today.month
53 @month_from ||= Date.today.month
54 @year_from ||= Date.today.year
54 @year_from ||= Date.today.year
55 end
55 end
56
56
57 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
57 zoom = (options[:zoom] || User.current.pref[:gantt_zoom]).to_i
58 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
58 @zoom = (zoom > 0 && zoom < 5) ? zoom : 2
59 months = (options[:months] || User.current.pref[:gantt_months]).to_i
59 months = (options[:months] || User.current.pref[:gantt_months]).to_i
60 @months = (months > 0 && months < 25) ? months : 6
60 @months = (months > 0 && months < 25) ? months : 6
61
61
62 # Save gantt parameters as user preference (zoom and months count)
62 # Save gantt parameters as user preference (zoom and months count)
63 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
63 if (User.current.logged? && (@zoom != User.current.pref[:gantt_zoom] || @months != User.current.pref[:gantt_months]))
64 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
64 User.current.pref[:gantt_zoom], User.current.pref[:gantt_months] = @zoom, @months
65 User.current.preference.save
65 User.current.preference.save
66 end
66 end
67
67
68 @date_from = Date.civil(@year_from, @month_from, 1)
68 @date_from = Date.civil(@year_from, @month_from, 1)
69 @date_to = (@date_from >> @months) - 1
69 @date_to = (@date_from >> @months) - 1
70
70
71 @subjects = ''
71 @subjects = ''
72 @lines = ''
72 @lines = ''
73 @number_of_rows = nil
73 @number_of_rows = nil
74 end
74 end
75
75
76 def common_params
76 def common_params
77 { :controller => 'gantts', :action => 'show', :project_id => @project }
77 { :controller => 'gantts', :action => 'show', :project_id => @project }
78 end
78 end
79
79
80 def params
80 def params
81 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
81 common_params.merge({ :zoom => zoom, :year => year_from, :month => month_from, :months => months })
82 end
82 end
83
83
84 def params_previous
84 def params_previous
85 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
85 common_params.merge({:year => (date_from << months).year, :month => (date_from << months).month, :zoom => zoom, :months => months })
86 end
86 end
87
87
88 def params_next
88 def params_next
89 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
89 common_params.merge({:year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months })
90 end
90 end
91
91
92 ### Extracted from the HTML view/helpers
92 ### Extracted from the HTML view/helpers
93 # Returns the number of rows that will be rendered on the Gantt chart
93 # Returns the number of rows that will be rendered on the Gantt chart
94 def number_of_rows
94 def number_of_rows
95 return @number_of_rows if @number_of_rows
95 return @number_of_rows if @number_of_rows
96
96
97 if @project
97 if @project
98 return number_of_rows_on_project(@project)
98 return number_of_rows_on_project(@project)
99 else
99 else
100 Project.roots.visible.inject(0) do |total, project|
100 Project.roots.visible.has_module('issue_tracking').inject(0) do |total, project|
101 total += number_of_rows_on_project(project)
101 total += number_of_rows_on_project(project)
102 end
102 end
103 end
103 end
104 end
104 end
105
105
106 # Returns the number of rows that will be used to list a project on
106 # Returns the number of rows that will be used to list a project on
107 # the Gantt chart. This will recurse for each subproject.
107 # the Gantt chart. This will recurse for each subproject.
108 def number_of_rows_on_project(project)
108 def number_of_rows_on_project(project)
109 # Remove the project requirement for Versions because it will
109 # Remove the project requirement for Versions because it will
110 # restrict issues to only be on the current project. This
110 # restrict issues to only be on the current project. This
111 # ends up missing issues which are assigned to shared versions.
111 # ends up missing issues which are assigned to shared versions.
112 @query.project = nil if @query.project
112 @query.project = nil if @query.project
113
113
114 # One Root project
114 # One Root project
115 count = 1
115 count = 1
116 # Issues without a Version
116 # Issues without a Version
117 count += project.issues.for_gantt.without_version.with_query(@query).count
117 count += project.issues.for_gantt.without_version.with_query(@query).count
118
118
119 # Versions
119 # Versions
120 count += project.versions.count
120 count += project.versions.count
121
121
122 # Issues on the Versions
122 # Issues on the Versions
123 project.versions.each do |version|
123 project.versions.each do |version|
124 count += version.fixed_issues.for_gantt.with_query(@query).count
124 count += version.fixed_issues.for_gantt.with_query(@query).count
125 end
125 end
126
126
127 # Subprojects
127 # Subprojects
128 project.children.visible.each do |subproject|
128 project.children.visible.has_module('issue_tracking').each do |subproject|
129 count += number_of_rows_on_project(subproject)
129 count += number_of_rows_on_project(subproject)
130 end
130 end
131
131
132 count
132 count
133 end
133 end
134
134
135 # Renders the subjects of the Gantt chart, the left side.
135 # Renders the subjects of the Gantt chart, the left side.
136 def subjects(options={})
136 def subjects(options={})
137 render(options.merge(:only => :subjects)) unless @subjects_rendered
137 render(options.merge(:only => :subjects)) unless @subjects_rendered
138 @subjects
138 @subjects
139 end
139 end
140
140
141 # Renders the lines of the Gantt chart, the right side
141 # Renders the lines of the Gantt chart, the right side
142 def lines(options={})
142 def lines(options={})
143 render(options.merge(:only => :lines)) unless @lines_rendered
143 render(options.merge(:only => :lines)) unless @lines_rendered
144 @lines
144 @lines
145 end
145 end
146
146
147 def render(options={})
147 def render(options={})
148 options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
148 options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
149
149
150 @subjects = '' unless options[:only] == :lines
150 @subjects = '' unless options[:only] == :lines
151 @lines = '' unless options[:only] == :subjects
151 @lines = '' unless options[:only] == :subjects
152 @number_of_rows = 0
152 @number_of_rows = 0
153
153
154 if @project
154 if @project
155 render_project(@project, options)
155 render_project(@project, options)
156 else
156 else
157 Project.roots.visible.each do |project|
157 Project.roots.visible.has_module('issue_tracking').each do |project|
158 render_project(project, options)
158 render_project(project, options)
159 end
159 end
160 end
160 end
161
161
162 @subjects_rendered = true unless options[:only] == :lines
162 @subjects_rendered = true unless options[:only] == :lines
163 @lines_rendered = true unless options[:only] == :subjects
163 @lines_rendered = true unless options[:only] == :subjects
164
164
165 render_end(options)
165 render_end(options)
166 end
166 end
167
167
168 def render_project(project, options={})
168 def render_project(project, options={})
169 options[:top] = 0 unless options.key? :top
169 options[:top] = 0 unless options.key? :top
170 options[:indent_increment] = 20 unless options.key? :indent_increment
170 options[:indent_increment] = 20 unless options.key? :indent_increment
171 options[:top_increment] = 20 unless options.key? :top_increment
171 options[:top_increment] = 20 unless options.key? :top_increment
172
172
173 subject_for_project(project, options) unless options[:only] == :lines
173 subject_for_project(project, options) unless options[:only] == :lines
174 line_for_project(project, options) unless options[:only] == :subjects
174 line_for_project(project, options) unless options[:only] == :subjects
175
175
176 options[:top] += options[:top_increment]
176 options[:top] += options[:top_increment]
177 options[:indent] += options[:indent_increment]
177 options[:indent] += options[:indent_increment]
178 @number_of_rows += 1
178 @number_of_rows += 1
179
179
180 # Second, Issues without a version
180 # Second, Issues without a version
181 issues = project.issues.for_gantt.without_version.with_query(@query)
181 issues = project.issues.for_gantt.without_version.with_query(@query)
182 sort_issues!(issues)
182 sort_issues!(issues)
183 if issues
183 if issues
184 render_issues(issues, options)
184 render_issues(issues, options)
185 end
185 end
186
186
187 # Third, Versions
187 # Third, Versions
188 project.versions.sort.each do |version|
188 project.versions.sort.each do |version|
189 render_version(version, options)
189 render_version(version, options)
190 end
190 end
191
191
192 # Fourth, subprojects
192 # Fourth, subprojects
193 project.children.visible.each do |project|
193 project.children.visible.has_module('issue_tracking').each do |project|
194 render_project(project, options)
194 render_project(project, options)
195 end
195 end
196
196
197 # Remove indent to hit the next sibling
197 # Remove indent to hit the next sibling
198 options[:indent] -= options[:indent_increment]
198 options[:indent] -= options[:indent_increment]
199 end
199 end
200
200
201 def render_issues(issues, options={})
201 def render_issues(issues, options={})
202 issues.each do |i|
202 issues.each do |i|
203 subject_for_issue(i, options) unless options[:only] == :lines
203 subject_for_issue(i, options) unless options[:only] == :lines
204 line_for_issue(i, options) unless options[:only] == :subjects
204 line_for_issue(i, options) unless options[:only] == :subjects
205
205
206 options[:top] += options[:top_increment]
206 options[:top] += options[:top_increment]
207 @number_of_rows += 1
207 @number_of_rows += 1
208 end
208 end
209 end
209 end
210
210
211 def render_version(version, options={})
211 def render_version(version, options={})
212 # Version header
212 # Version header
213 subject_for_version(version, options) unless options[:only] == :lines
213 subject_for_version(version, options) unless options[:only] == :lines
214 line_for_version(version, options) unless options[:only] == :subjects
214 line_for_version(version, options) unless options[:only] == :subjects
215
215
216 options[:top] += options[:top_increment]
216 options[:top] += options[:top_increment]
217 @number_of_rows += 1
217 @number_of_rows += 1
218
218
219 # Remove the project requirement for Versions because it will
219 # Remove the project requirement for Versions because it will
220 # restrict issues to only be on the current project. This
220 # restrict issues to only be on the current project. This
221 # ends up missing issues which are assigned to shared versions.
221 # ends up missing issues which are assigned to shared versions.
222 @query.project = nil if @query.project
222 @query.project = nil if @query.project
223
223
224 issues = version.fixed_issues.for_gantt.with_query(@query)
224 issues = version.fixed_issues.for_gantt.with_query(@query)
225 if issues
225 if issues
226 sort_issues!(issues)
226 sort_issues!(issues)
227 # Indent issues
227 # Indent issues
228 options[:indent] += options[:indent_increment]
228 options[:indent] += options[:indent_increment]
229 render_issues(issues, options)
229 render_issues(issues, options)
230 options[:indent] -= options[:indent_increment]
230 options[:indent] -= options[:indent_increment]
231 end
231 end
232 end
232 end
233
233
234 def render_end(options={})
234 def render_end(options={})
235 case options[:format]
235 case options[:format]
236 when :pdf
236 when :pdf
237 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
237 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
238 end
238 end
239 end
239 end
240
240
241 def subject_for_project(project, options)
241 def subject_for_project(project, options)
242 case options[:format]
242 case options[:format]
243 when :html
243 when :html
244 output = ''
244 output = ''
245
245
246 output << "<div class='project-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
246 output << "<div class='project-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
247 if project.is_a? Project
247 if project.is_a? Project
248 output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
248 output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
249 output << view.link_to_project(project)
249 output << view.link_to_project(project)
250 output << '</span>'
250 output << '</span>'
251 else
251 else
252 ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project"
252 ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project"
253 ''
253 ''
254 end
254 end
255 output << "</small></div>"
255 output << "</small></div>"
256 @subjects << output
256 @subjects << output
257 output
257 output
258 when :image
258 when :image
259
259
260 options[:image].fill('black')
260 options[:image].fill('black')
261 options[:image].stroke('transparent')
261 options[:image].stroke('transparent')
262 options[:image].stroke_width(1)
262 options[:image].stroke_width(1)
263 options[:image].text(options[:indent], options[:top] + 2, project.name)
263 options[:image].text(options[:indent], options[:top] + 2, project.name)
264 when :pdf
264 when :pdf
265 pdf_new_page?(options)
265 pdf_new_page?(options)
266 options[:pdf].SetY(options[:top])
266 options[:pdf].SetY(options[:top])
267 options[:pdf].SetX(15)
267 options[:pdf].SetX(15)
268
268
269 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
269 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
270 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
270 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
271
271
272 options[:pdf].SetY(options[:top])
272 options[:pdf].SetY(options[:top])
273 options[:pdf].SetX(options[:subject_width])
273 options[:pdf].SetX(options[:subject_width])
274 options[:pdf].Cell(options[:g_width], 5, "", "LR")
274 options[:pdf].Cell(options[:g_width], 5, "", "LR")
275 end
275 end
276 end
276 end
277
277
278 def line_for_project(project, options)
278 def line_for_project(project, options)
279 # Skip versions that don't have a start_date or due date
279 # Skip versions that don't have a start_date or due date
280 if project.is_a?(Project) && project.start_date && project.due_date
280 if project.is_a?(Project) && project.start_date && project.due_date
281 options[:zoom] ||= 1
281 options[:zoom] ||= 1
282 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
282 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
283
283
284
284
285 case options[:format]
285 case options[:format]
286 when :html
286 when :html
287 output = ''
287 output = ''
288 i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
288 i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
289
289
290 start_date = project.start_date
290 start_date = project.start_date
291 start_date ||= self.date_from
291 start_date ||= self.date_from
292 start_left = ((start_date - self.date_from)*options[:zoom]).floor
292 start_left = ((start_date - self.date_from)*options[:zoom]).floor
293
293
294 i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
294 i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
295 i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
295 i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
296 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
296 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
297 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
297 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
298
298
299 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
299 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
300 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
300 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
301
301
302 i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
302 i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
303 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
303 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
304 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
304 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
305
305
306 # Bar graphic
306 # Bar graphic
307
307
308 # Make sure that negative i_left and i_width don't
308 # Make sure that negative i_left and i_width don't
309 # overflow the subject
309 # overflow the subject
310 if i_end > 0 && i_left <= options[:g_width]
310 if i_end > 0 && i_left <= options[:g_width]
311 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
311 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
312 end
312 end
313
313
314 if l_width > 0 && i_left <= options[:g_width]
314 if l_width > 0 && i_left <= options[:g_width]
315 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
315 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
316 end
316 end
317 if d_width > 0 && i_left <= options[:g_width]
317 if d_width > 0 && i_left <= options[:g_width]
318 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
318 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
319 end
319 end
320
320
321
321
322 # Starting diamond
322 # Starting diamond
323 if start_left <= options[:g_width] && start_left > 0
323 if start_left <= options[:g_width] && start_left > 0
324 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
324 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
325 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
325 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
326 output << "</div>"
326 output << "</div>"
327 end
327 end
328
328
329 # Ending diamond
329 # Ending diamond
330 # Don't show items too far ahead
330 # Don't show items too far ahead
331 if i_end <= options[:g_width] && i_end > 0
331 if i_end <= options[:g_width] && i_end > 0
332 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
332 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
333 end
333 end
334
334
335 # DIsplay the Project name and %
335 # DIsplay the Project name and %
336 if i_end <= options[:g_width]
336 if i_end <= options[:g_width]
337 # Display the status even if it's floated off to the left
337 # Display the status even if it's floated off to the left
338 status_px = i_end + 12 # 12px for the diamond
338 status_px = i_end + 12 # 12px for the diamond
339 status_px = 0 if status_px <= 0
339 status_px = 0 if status_px <= 0
340
340
341 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
341 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
342 output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
342 output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
343 output << "</div>"
343 output << "</div>"
344 end
344 end
345 @lines << output
345 @lines << output
346 output
346 output
347 when :image
347 when :image
348 options[:image].stroke('transparent')
348 options[:image].stroke('transparent')
349 i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
349 i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
350
350
351 # Make sure negative i_left doesn't overflow the subject
351 # Make sure negative i_left doesn't overflow the subject
352 if i_left > options[:subject_width]
352 if i_left > options[:subject_width]
353 options[:image].fill('blue')
353 options[:image].fill('blue')
354 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
354 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
355 options[:image].fill('black')
355 options[:image].fill('black')
356 options[:image].text(i_left + 11, options[:top] + 1, project.name)
356 options[:image].text(i_left + 11, options[:top] + 1, project.name)
357 end
357 end
358 when :pdf
358 when :pdf
359 options[:pdf].SetY(options[:top]+1.5)
359 options[:pdf].SetY(options[:top]+1.5)
360 i_left = ((project.due_date - @date_from)*options[:zoom])
360 i_left = ((project.due_date - @date_from)*options[:zoom])
361
361
362 # Make sure negative i_left doesn't overflow the subject
362 # Make sure negative i_left doesn't overflow the subject
363 if i_left > 0
363 if i_left > 0
364 options[:pdf].SetX(options[:subject_width] + i_left)
364 options[:pdf].SetX(options[:subject_width] + i_left)
365 options[:pdf].SetFillColor(50,50,200)
365 options[:pdf].SetFillColor(50,50,200)
366 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
366 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
367
367
368 options[:pdf].SetY(options[:top]+1.5)
368 options[:pdf].SetY(options[:top]+1.5)
369 options[:pdf].SetX(options[:subject_width] + i_left + 3)
369 options[:pdf].SetX(options[:subject_width] + i_left + 3)
370 options[:pdf].Cell(30, 2, "#{project.name}")
370 options[:pdf].Cell(30, 2, "#{project.name}")
371 end
371 end
372 end
372 end
373 else
373 else
374 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
374 ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
375 ''
375 ''
376 end
376 end
377 end
377 end
378
378
379 def subject_for_version(version, options)
379 def subject_for_version(version, options)
380 case options[:format]
380 case options[:format]
381 when :html
381 when :html
382 output = ''
382 output = ''
383 output << "<div class='version-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
383 output << "<div class='version-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
384 if version.is_a? Version
384 if version.is_a? Version
385 output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
385 output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
386 output << view.link_to_version(version)
386 output << view.link_to_version(version)
387 output << '</span>'
387 output << '</span>'
388 else
388 else
389 ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
389 ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
390 ''
390 ''
391 end
391 end
392 output << "</small></div>"
392 output << "</small></div>"
393 @subjects << output
393 @subjects << output
394 output
394 output
395 when :image
395 when :image
396 options[:image].fill('black')
396 options[:image].fill('black')
397 options[:image].stroke('transparent')
397 options[:image].stroke('transparent')
398 options[:image].stroke_width(1)
398 options[:image].stroke_width(1)
399 options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project)
399 options[:image].text(options[:indent], options[:top] + 2, version.to_s_with_project)
400 when :pdf
400 when :pdf
401 pdf_new_page?(options)
401 pdf_new_page?(options)
402 options[:pdf].SetY(options[:top])
402 options[:pdf].SetY(options[:top])
403 options[:pdf].SetX(15)
403 options[:pdf].SetX(15)
404
404
405 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
405 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
406 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
406 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.to_s_with_project}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
407
407
408 options[:pdf].SetY(options[:top])
408 options[:pdf].SetY(options[:top])
409 options[:pdf].SetX(options[:subject_width])
409 options[:pdf].SetX(options[:subject_width])
410 options[:pdf].Cell(options[:g_width], 5, "", "LR")
410 options[:pdf].Cell(options[:g_width], 5, "", "LR")
411 end
411 end
412 end
412 end
413
413
414 def line_for_version(version, options)
414 def line_for_version(version, options)
415 # Skip versions that don't have a start_date
415 # Skip versions that don't have a start_date
416 if version.is_a?(Version) && version.start_date && version.due_date
416 if version.is_a?(Version) && version.start_date && version.due_date
417 options[:zoom] ||= 1
417 options[:zoom] ||= 1
418 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
418 options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
419
419
420 case options[:format]
420 case options[:format]
421 when :html
421 when :html
422 output = ''
422 output = ''
423 i_left = ((version.start_date - self.date_from)*options[:zoom]).floor
423 i_left = ((version.start_date - self.date_from)*options[:zoom]).floor
424 # TODO: or version.fixed_issues.collect(&:start_date).min
424 # TODO: or version.fixed_issues.collect(&:start_date).min
425 start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present?
425 start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present?
426 start_date ||= self.date_from
426 start_date ||= self.date_from
427 start_left = ((start_date - self.date_from)*options[:zoom]).floor
427 start_left = ((start_date - self.date_from)*options[:zoom]).floor
428
428
429 i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to )
429 i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to )
430 i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor
430 i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor
431 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
431 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
432 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
432 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
433
433
434 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
434 i_late_date = [i_end_date, Date.today].min if start_date < Date.today
435
435
436 i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
436 i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
437 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
437 d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
438 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
438 l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
439
439
440 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel
440 i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel
441
441
442 # Bar graphic
442 # Bar graphic
443
443
444 # Make sure that negative i_left and i_width don't
444 # Make sure that negative i_left and i_width don't
445 # overflow the subject
445 # overflow the subject
446 if i_width > 0 && i_left <= options[:g_width]
446 if i_width > 0 && i_left <= options[:g_width]
447 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'>&nbsp;</div>"
447 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'>&nbsp;</div>"
448 end
448 end
449 if l_width > 0 && i_left <= options[:g_width]
449 if l_width > 0 && i_left <= options[:g_width]
450 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'>&nbsp;</div>"
450 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'>&nbsp;</div>"
451 end
451 end
452 if d_width > 0 && i_left <= options[:g_width]
452 if d_width > 0 && i_left <= options[:g_width]
453 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'>&nbsp;</div>"
453 output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'>&nbsp;</div>"
454 end
454 end
455
455
456
456
457 # Starting diamond
457 # Starting diamond
458 if start_left <= options[:g_width] && start_left > 0
458 if start_left <= options[:g_width] && start_left > 0
459 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'>&nbsp;</div>"
459 output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'>&nbsp;</div>"
460 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>"
460 output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>"
461 output << "</div>"
461 output << "</div>"
462 end
462 end
463
463
464 # Ending diamond
464 # Ending diamond
465 # Don't show items too far ahead
465 # Don't show items too far ahead
466 if i_left <= options[:g_width] && i_end > 0
466 if i_left <= options[:g_width] && i_end > 0
467 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'>&nbsp;</div>"
467 output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'>&nbsp;</div>"
468 end
468 end
469
469
470 # Display the Version name and %
470 # Display the Version name and %
471 if i_end <= options[:g_width]
471 if i_end <= options[:g_width]
472 # Display the status even if it's floated off to the left
472 # Display the status even if it's floated off to the left
473 status_px = i_end + 12 # 12px for the diamond
473 status_px = i_end + 12 # 12px for the diamond
474 status_px = 0 if status_px <= 0
474 status_px = 0 if status_px <= 0
475
475
476 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>"
476 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>"
477 output << h("#{version.project} -") unless @project && @project == version.project
477 output << h("#{version.project} -") unless @project && @project == version.project
478 output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>"
478 output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>"
479 output << "</div>"
479 output << "</div>"
480 end
480 end
481 @lines << output
481 @lines << output
482 output
482 output
483 when :image
483 when :image
484 options[:image].stroke('transparent')
484 options[:image].stroke('transparent')
485 i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
485 i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
486
486
487 # Make sure negative i_left doesn't overflow the subject
487 # Make sure negative i_left doesn't overflow the subject
488 if i_left > options[:subject_width]
488 if i_left > options[:subject_width]
489 options[:image].fill('green')
489 options[:image].fill('green')
490 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
490 options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
491 options[:image].fill('black')
491 options[:image].fill('black')
492 options[:image].text(i_left + 11, options[:top] + 1, version.name)
492 options[:image].text(i_left + 11, options[:top] + 1, version.name)
493 end
493 end
494 when :pdf
494 when :pdf
495 options[:pdf].SetY(options[:top]+1.5)
495 options[:pdf].SetY(options[:top]+1.5)
496 i_left = ((version.start_date - @date_from)*options[:zoom])
496 i_left = ((version.start_date - @date_from)*options[:zoom])
497
497
498 # Make sure negative i_left doesn't overflow the subject
498 # Make sure negative i_left doesn't overflow the subject
499 if i_left > 0
499 if i_left > 0
500 options[:pdf].SetX(options[:subject_width] + i_left)
500 options[:pdf].SetX(options[:subject_width] + i_left)
501 options[:pdf].SetFillColor(50,200,50)
501 options[:pdf].SetFillColor(50,200,50)
502 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
502 options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
503
503
504 options[:pdf].SetY(options[:top]+1.5)
504 options[:pdf].SetY(options[:top]+1.5)
505 options[:pdf].SetX(options[:subject_width] + i_left + 3)
505 options[:pdf].SetX(options[:subject_width] + i_left + 3)
506 options[:pdf].Cell(30, 2, "#{version.name}")
506 options[:pdf].Cell(30, 2, "#{version.name}")
507 end
507 end
508 end
508 end
509 else
509 else
510 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
510 ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
511 ''
511 ''
512 end
512 end
513 end
513 end
514
514
515 def subject_for_issue(issue, options)
515 def subject_for_issue(issue, options)
516 case options[:format]
516 case options[:format]
517 when :html
517 when :html
518 output = ''
518 output = ''
519 output << "<div class='tooltip'>"
519 output << "<div class='tooltip'>"
520 output << "<div class='issue-subject' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
520 output << "<div class='issue-subject' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
521 if issue.is_a? Issue
521 if issue.is_a? Issue
522 css_classes = []
522 css_classes = []
523 css_classes << 'issue-overdue' if issue.overdue?
523 css_classes << 'issue-overdue' if issue.overdue?
524 css_classes << 'issue-behind-schedule' if issue.behind_schedule?
524 css_classes << 'issue-behind-schedule' if issue.behind_schedule?
525 css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
525 css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
526
526
527 if issue.assigned_to.present?
527 if issue.assigned_to.present?
528 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
528 assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
529 output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
529 output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
530 end
530 end
531 output << "<span class='#{css_classes.join(' ')}'>"
531 output << "<span class='#{css_classes.join(' ')}'>"
532 output << view.link_to_issue(issue)
532 output << view.link_to_issue(issue)
533 output << '</span>'
533 output << '</span>'
534 else
534 else
535 ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue"
535 ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue"
536 ''
536 ''
537 end
537 end
538 output << "</small></div>"
538 output << "</small></div>"
539
539
540 # Tooltip
540 # Tooltip
541 if issue.is_a? Issue
541 if issue.is_a? Issue
542 output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>"
542 output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>"
543 output << view.render_issue_tooltip(issue)
543 output << view.render_issue_tooltip(issue)
544 output << "</span>"
544 output << "</span>"
545 end
545 end
546
546
547 output << "</div>"
547 output << "</div>"
548 @subjects << output
548 @subjects << output
549 output
549 output
550 when :image
550 when :image
551 options[:image].fill('black')
551 options[:image].fill('black')
552 options[:image].stroke('transparent')
552 options[:image].stroke('transparent')
553 options[:image].stroke_width(1)
553 options[:image].stroke_width(1)
554 options[:image].text(options[:indent], options[:top] + 2, issue.subject)
554 options[:image].text(options[:indent], options[:top] + 2, issue.subject)
555 when :pdf
555 when :pdf
556 pdf_new_page?(options)
556 pdf_new_page?(options)
557 options[:pdf].SetY(options[:top])
557 options[:pdf].SetY(options[:top])
558 options[:pdf].SetX(15)
558 options[:pdf].SetX(15)
559
559
560 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
560 char_limit = PDF::MaxCharactorsForSubject - options[:indent]
561 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
561 options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
562
562
563 options[:pdf].SetY(options[:top])
563 options[:pdf].SetY(options[:top])
564 options[:pdf].SetX(options[:subject_width])
564 options[:pdf].SetX(options[:subject_width])
565 options[:pdf].Cell(options[:g_width], 5, "", "LR")
565 options[:pdf].Cell(options[:g_width], 5, "", "LR")
566 end
566 end
567 end
567 end
568
568
569 def line_for_issue(issue, options)
569 def line_for_issue(issue, options)
570 # Skip issues that don't have a due_before (due_date or version's due_date)
570 # Skip issues that don't have a due_before (due_date or version's due_date)
571 if issue.is_a?(Issue) && issue.due_before
571 if issue.is_a?(Issue) && issue.due_before
572 case options[:format]
572 case options[:format]
573 when :html
573 when :html
574 output = ''
574 output = ''
575 # Handle nil start_dates, rare but can happen.
575 # Handle nil start_dates, rare but can happen.
576 i_start_date = if issue.start_date && issue.start_date >= self.date_from
576 i_start_date = if issue.start_date && issue.start_date >= self.date_from
577 issue.start_date
577 issue.start_date
578 else
578 else
579 self.date_from
579 self.date_from
580 end
580 end
581
581
582 i_end_date = ((issue.due_before && issue.due_before <= self.date_to) ? issue.due_before : self.date_to )
582 i_end_date = ((issue.due_before && issue.due_before <= self.date_to) ? issue.due_before : self.date_to )
583 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
583 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
584 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
584 i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
585 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
585 i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
586
586
587 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
587 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
588
588
589 i_left = ((i_start_date - self.date_from)*options[:zoom]).floor
589 i_left = ((i_start_date - self.date_from)*options[:zoom]).floor
590 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor - 2 # total width of the issue (- 2 for left and right borders)
590 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor - 2 # total width of the issue (- 2 for left and right borders)
591 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor - 2 # done width
591 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor - 2 # done width
592 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
592 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
593 css = "task " + (issue.leaf? ? 'leaf' : 'parent')
593 css = "task " + (issue.leaf? ? 'leaf' : 'parent')
594
594
595 # Make sure that negative i_left and i_width don't
595 # Make sure that negative i_left and i_width don't
596 # overflow the subject
596 # overflow the subject
597 if i_width > 0
597 if i_width > 0
598 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;' class='#{css} task_todo'>&nbsp;</div>"
598 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;' class='#{css} task_todo'>&nbsp;</div>"
599 end
599 end
600 if l_width > 0
600 if l_width > 0
601 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ l_width }px;' class='#{css} task_late'>&nbsp;</div>"
601 output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ l_width }px;' class='#{css} task_late'>&nbsp;</div>"
602 end
602 end
603 if d_width > 0
603 if d_width > 0
604 output<< "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ d_width }px;' class='#{css} task_done'>&nbsp;</div>"
604 output<< "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ d_width }px;' class='#{css} task_done'>&nbsp;</div>"
605 end
605 end
606
606
607 # Display the status even if it's floated off to the left
607 # Display the status even if it's floated off to the left
608 status_px = i_left + i_width + 5
608 status_px = i_left + i_width + 5
609 status_px = 5 if status_px <= 0
609 status_px = 5 if status_px <= 0
610
610
611 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='#{css} label issue-name'>"
611 output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='#{css} label issue-name'>"
612 output << issue.status.name
612 output << issue.status.name
613 output << ' '
613 output << ' '
614 output << (issue.done_ratio).to_i.to_s
614 output << (issue.done_ratio).to_i.to_s
615 output << "%"
615 output << "%"
616 output << "</div>"
616 output << "</div>"
617
617
618 output << "<div class='tooltip' style='position: absolute;top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;height:12px;'>"
618 output << "<div class='tooltip' style='position: absolute;top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;height:12px;'>"
619 output << '<span class="tip">'
619 output << '<span class="tip">'
620 output << view.render_issue_tooltip(issue)
620 output << view.render_issue_tooltip(issue)
621 output << "</span></div>"
621 output << "</span></div>"
622 @lines << output
622 @lines << output
623 output
623 output
624
624
625 when :image
625 when :image
626 # Handle nil start_dates, rare but can happen.
626 # Handle nil start_dates, rare but can happen.
627 i_start_date = if issue.start_date && issue.start_date >= @date_from
627 i_start_date = if issue.start_date && issue.start_date >= @date_from
628 issue.start_date
628 issue.start_date
629 else
629 else
630 @date_from
630 @date_from
631 end
631 end
632
632
633 i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to )
633 i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to )
634 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
634 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
635 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
635 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
636 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
636 i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
637 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
637 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
638
638
639 i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor
639 i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor
640 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue
640 i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue
641 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width
641 d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width
642 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width
642 l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width
643
643
644
644
645 # Make sure that negative i_left and i_width don't
645 # Make sure that negative i_left and i_width don't
646 # overflow the subject
646 # overflow the subject
647 if i_width > 0
647 if i_width > 0
648 options[:image].fill('grey')
648 options[:image].fill('grey')
649 options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6)
649 options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6)
650 options[:image].fill('red')
650 options[:image].fill('red')
651 options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0
651 options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0
652 options[:image].fill('blue')
652 options[:image].fill('blue')
653 options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0
653 options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0
654 end
654 end
655
655
656 # Show the status and % done next to the subject if it overflows
656 # Show the status and % done next to the subject if it overflows
657 options[:image].fill('black')
657 options[:image].fill('black')
658 if i_width > 0
658 if i_width > 0
659 options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
659 options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
660 else
660 else
661 options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
661 options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
662 end
662 end
663
663
664 when :pdf
664 when :pdf
665 options[:pdf].SetY(options[:top]+1.5)
665 options[:pdf].SetY(options[:top]+1.5)
666 # Handle nil start_dates, rare but can happen.
666 # Handle nil start_dates, rare but can happen.
667 i_start_date = if issue.start_date && issue.start_date >= @date_from
667 i_start_date = if issue.start_date && issue.start_date >= @date_from
668 issue.start_date
668 issue.start_date
669 else
669 else
670 @date_from
670 @date_from
671 end
671 end
672
672
673 i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to )
673 i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to )
674
674
675 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
675 i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
676 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
676 i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
677 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
677 i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
678
678
679 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
679 i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
680
680
681 i_left = ((i_start_date - @date_from)*options[:zoom])
681 i_left = ((i_start_date - @date_from)*options[:zoom])
682 i_width = ((i_end_date - i_start_date + 1)*options[:zoom])
682 i_width = ((i_end_date - i_start_date + 1)*options[:zoom])
683 d_width = ((i_done_date - i_start_date)*options[:zoom])
683 d_width = ((i_done_date - i_start_date)*options[:zoom])
684 l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date
684 l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date
685 l_width ||= 0
685 l_width ||= 0
686
686
687 # Make sure that negative i_left and i_width don't
687 # Make sure that negative i_left and i_width don't
688 # overflow the subject
688 # overflow the subject
689 if i_width > 0
689 if i_width > 0
690 options[:pdf].SetX(options[:subject_width] + i_left)
690 options[:pdf].SetX(options[:subject_width] + i_left)
691 options[:pdf].SetFillColor(200,200,200)
691 options[:pdf].SetFillColor(200,200,200)
692 options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1)
692 options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1)
693 end
693 end
694
694
695 if l_width > 0
695 if l_width > 0
696 options[:pdf].SetY(options[:top]+1.5)
696 options[:pdf].SetY(options[:top]+1.5)
697 options[:pdf].SetX(options[:subject_width] + i_left)
697 options[:pdf].SetX(options[:subject_width] + i_left)
698 options[:pdf].SetFillColor(255,100,100)
698 options[:pdf].SetFillColor(255,100,100)
699 options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1)
699 options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1)
700 end
700 end
701 if d_width > 0
701 if d_width > 0
702 options[:pdf].SetY(options[:top]+1.5)
702 options[:pdf].SetY(options[:top]+1.5)
703 options[:pdf].SetX(options[:subject_width] + i_left)
703 options[:pdf].SetX(options[:subject_width] + i_left)
704 options[:pdf].SetFillColor(100,100,255)
704 options[:pdf].SetFillColor(100,100,255)
705 options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1)
705 options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1)
706 end
706 end
707
707
708 options[:pdf].SetY(options[:top]+1.5)
708 options[:pdf].SetY(options[:top]+1.5)
709
709
710 # Make sure that negative i_left and i_width don't
710 # Make sure that negative i_left and i_width don't
711 # overflow the subject
711 # overflow the subject
712 if (i_left + i_width) >= 0
712 if (i_left + i_width) >= 0
713 options[:pdf].SetX(options[:subject_width] + i_left + i_width)
713 options[:pdf].SetX(options[:subject_width] + i_left + i_width)
714 else
714 else
715 options[:pdf].SetX(options[:subject_width])
715 options[:pdf].SetX(options[:subject_width])
716 end
716 end
717 options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%")
717 options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%")
718 end
718 end
719 else
719 else
720 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
720 ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
721 ''
721 ''
722 end
722 end
723 end
723 end
724
724
725 # Generates a gantt image
725 # Generates a gantt image
726 # Only defined if RMagick is avalaible
726 # Only defined if RMagick is avalaible
727 def to_image(format='PNG')
727 def to_image(format='PNG')
728 date_to = (@date_from >> @months)-1
728 date_to = (@date_from >> @months)-1
729 show_weeks = @zoom > 1
729 show_weeks = @zoom > 1
730 show_days = @zoom > 2
730 show_days = @zoom > 2
731
731
732 subject_width = 400
732 subject_width = 400
733 header_heigth = 18
733 header_heigth = 18
734 # width of one day in pixels
734 # width of one day in pixels
735 zoom = @zoom*2
735 zoom = @zoom*2
736 g_width = (@date_to - @date_from + 1)*zoom
736 g_width = (@date_to - @date_from + 1)*zoom
737 g_height = 20 * number_of_rows + 30
737 g_height = 20 * number_of_rows + 30
738 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
738 headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
739 height = g_height + headers_heigth
739 height = g_height + headers_heigth
740
740
741 imgl = Magick::ImageList.new
741 imgl = Magick::ImageList.new
742 imgl.new_image(subject_width+g_width+1, height)
742 imgl.new_image(subject_width+g_width+1, height)
743 gc = Magick::Draw.new
743 gc = Magick::Draw.new
744
744
745 # Subjects
745 # Subjects
746 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
746 subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
747
747
748 # Months headers
748 # Months headers
749 month_f = @date_from
749 month_f = @date_from
750 left = subject_width
750 left = subject_width
751 @months.times do
751 @months.times do
752 width = ((month_f >> 1) - month_f) * zoom
752 width = ((month_f >> 1) - month_f) * zoom
753 gc.fill('white')
753 gc.fill('white')
754 gc.stroke('grey')
754 gc.stroke('grey')
755 gc.stroke_width(1)
755 gc.stroke_width(1)
756 gc.rectangle(left, 0, left + width, height)
756 gc.rectangle(left, 0, left + width, height)
757 gc.fill('black')
757 gc.fill('black')
758 gc.stroke('transparent')
758 gc.stroke('transparent')
759 gc.stroke_width(1)
759 gc.stroke_width(1)
760 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
760 gc.text(left.round + 8, 14, "#{month_f.year}-#{month_f.month}")
761 left = left + width
761 left = left + width
762 month_f = month_f >> 1
762 month_f = month_f >> 1
763 end
763 end
764
764
765 # Weeks headers
765 # Weeks headers
766 if show_weeks
766 if show_weeks
767 left = subject_width
767 left = subject_width
768 height = header_heigth
768 height = header_heigth
769 if @date_from.cwday == 1
769 if @date_from.cwday == 1
770 # date_from is monday
770 # date_from is monday
771 week_f = date_from
771 week_f = date_from
772 else
772 else
773 # find next monday after date_from
773 # find next monday after date_from
774 week_f = @date_from + (7 - @date_from.cwday + 1)
774 week_f = @date_from + (7 - @date_from.cwday + 1)
775 width = (7 - @date_from.cwday + 1) * zoom
775 width = (7 - @date_from.cwday + 1) * zoom
776 gc.fill('white')
776 gc.fill('white')
777 gc.stroke('grey')
777 gc.stroke('grey')
778 gc.stroke_width(1)
778 gc.stroke_width(1)
779 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
779 gc.rectangle(left, header_heigth, left + width, 2*header_heigth + g_height-1)
780 left = left + width
780 left = left + width
781 end
781 end
782 while week_f <= date_to
782 while week_f <= date_to
783 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
783 width = (week_f + 6 <= date_to) ? 7 * zoom : (date_to - week_f + 1) * zoom
784 gc.fill('white')
784 gc.fill('white')
785 gc.stroke('grey')
785 gc.stroke('grey')
786 gc.stroke_width(1)
786 gc.stroke_width(1)
787 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
787 gc.rectangle(left.round, header_heigth, left.round + width, 2*header_heigth + g_height-1)
788 gc.fill('black')
788 gc.fill('black')
789 gc.stroke('transparent')
789 gc.stroke('transparent')
790 gc.stroke_width(1)
790 gc.stroke_width(1)
791 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
791 gc.text(left.round + 2, header_heigth + 14, week_f.cweek.to_s)
792 left = left + width
792 left = left + width
793 week_f = week_f+7
793 week_f = week_f+7
794 end
794 end
795 end
795 end
796
796
797 # Days details (week-end in grey)
797 # Days details (week-end in grey)
798 if show_days
798 if show_days
799 left = subject_width
799 left = subject_width
800 height = g_height + header_heigth - 1
800 height = g_height + header_heigth - 1
801 wday = @date_from.cwday
801 wday = @date_from.cwday
802 (date_to - @date_from + 1).to_i.times do
802 (date_to - @date_from + 1).to_i.times do
803 width = zoom
803 width = zoom
804 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
804 gc.fill(wday == 6 || wday == 7 ? '#eee' : 'white')
805 gc.stroke('grey')
805 gc.stroke('grey')
806 gc.stroke_width(1)
806 gc.stroke_width(1)
807 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
807 gc.rectangle(left, 2*header_heigth, left + width, 2*header_heigth + g_height-1)
808 left = left + width
808 left = left + width
809 wday = wday + 1
809 wday = wday + 1
810 wday = 1 if wday > 7
810 wday = 1 if wday > 7
811 end
811 end
812 end
812 end
813
813
814 # border
814 # border
815 gc.fill('transparent')
815 gc.fill('transparent')
816 gc.stroke('grey')
816 gc.stroke('grey')
817 gc.stroke_width(1)
817 gc.stroke_width(1)
818 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
818 gc.rectangle(0, 0, subject_width+g_width, headers_heigth)
819 gc.stroke('black')
819 gc.stroke('black')
820 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
820 gc.rectangle(0, 0, subject_width+g_width, g_height+ headers_heigth-1)
821
821
822 # content
822 # content
823 top = headers_heigth + 20
823 top = headers_heigth + 20
824
824
825 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
825 lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
826
826
827 # today red line
827 # today red line
828 if Date.today >= @date_from and Date.today <= date_to
828 if Date.today >= @date_from and Date.today <= date_to
829 gc.stroke('red')
829 gc.stroke('red')
830 x = (Date.today-@date_from+1)*zoom + subject_width
830 x = (Date.today-@date_from+1)*zoom + subject_width
831 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
831 gc.line(x, headers_heigth, x, headers_heigth + g_height-1)
832 end
832 end
833
833
834 gc.draw(imgl)
834 gc.draw(imgl)
835 imgl.format = format
835 imgl.format = format
836 imgl.to_blob
836 imgl.to_blob
837 end if Object.const_defined?(:Magick)
837 end if Object.const_defined?(:Magick)
838
838
839 def to_pdf
839 def to_pdf
840 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
840 pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
841 pdf.SetTitle("#{l(:label_gantt)} #{project}")
841 pdf.SetTitle("#{l(:label_gantt)} #{project}")
842 pdf.AliasNbPages
842 pdf.AliasNbPages
843 pdf.footer_date = format_date(Date.today)
843 pdf.footer_date = format_date(Date.today)
844 pdf.AddPage("L")
844 pdf.AddPage("L")
845 pdf.SetFontStyle('B',12)
845 pdf.SetFontStyle('B',12)
846 pdf.SetX(15)
846 pdf.SetX(15)
847 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
847 pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
848 pdf.Ln
848 pdf.Ln
849 pdf.SetFontStyle('B',9)
849 pdf.SetFontStyle('B',9)
850
850
851 subject_width = PDF::LeftPaneWidth
851 subject_width = PDF::LeftPaneWidth
852 header_heigth = 5
852 header_heigth = 5
853
853
854 headers_heigth = header_heigth
854 headers_heigth = header_heigth
855 show_weeks = false
855 show_weeks = false
856 show_days = false
856 show_days = false
857
857
858 if self.months < 7
858 if self.months < 7
859 show_weeks = true
859 show_weeks = true
860 headers_heigth = 2*header_heigth
860 headers_heigth = 2*header_heigth
861 if self.months < 3
861 if self.months < 3
862 show_days = true
862 show_days = true
863 headers_heigth = 3*header_heigth
863 headers_heigth = 3*header_heigth
864 end
864 end
865 end
865 end
866
866
867 g_width = PDF.right_pane_width
867 g_width = PDF.right_pane_width
868 zoom = (g_width) / (self.date_to - self.date_from + 1)
868 zoom = (g_width) / (self.date_to - self.date_from + 1)
869 g_height = 120
869 g_height = 120
870 t_height = g_height + headers_heigth
870 t_height = g_height + headers_heigth
871
871
872 y_start = pdf.GetY
872 y_start = pdf.GetY
873
873
874 # Months headers
874 # Months headers
875 month_f = self.date_from
875 month_f = self.date_from
876 left = subject_width
876 left = subject_width
877 height = header_heigth
877 height = header_heigth
878 self.months.times do
878 self.months.times do
879 width = ((month_f >> 1) - month_f) * zoom
879 width = ((month_f >> 1) - month_f) * zoom
880 pdf.SetY(y_start)
880 pdf.SetY(y_start)
881 pdf.SetX(left)
881 pdf.SetX(left)
882 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
882 pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
883 left = left + width
883 left = left + width
884 month_f = month_f >> 1
884 month_f = month_f >> 1
885 end
885 end
886
886
887 # Weeks headers
887 # Weeks headers
888 if show_weeks
888 if show_weeks
889 left = subject_width
889 left = subject_width
890 height = header_heigth
890 height = header_heigth
891 if self.date_from.cwday == 1
891 if self.date_from.cwday == 1
892 # self.date_from is monday
892 # self.date_from is monday
893 week_f = self.date_from
893 week_f = self.date_from
894 else
894 else
895 # find next monday after self.date_from
895 # find next monday after self.date_from
896 week_f = self.date_from + (7 - self.date_from.cwday + 1)
896 week_f = self.date_from + (7 - self.date_from.cwday + 1)
897 width = (7 - self.date_from.cwday + 1) * zoom-1
897 width = (7 - self.date_from.cwday + 1) * zoom-1
898 pdf.SetY(y_start + header_heigth)
898 pdf.SetY(y_start + header_heigth)
899 pdf.SetX(left)
899 pdf.SetX(left)
900 pdf.Cell(width + 1, height, "", "LTR")
900 pdf.Cell(width + 1, height, "", "LTR")
901 left = left + width+1
901 left = left + width+1
902 end
902 end
903 while week_f <= self.date_to
903 while week_f <= self.date_to
904 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
904 width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
905 pdf.SetY(y_start + header_heigth)
905 pdf.SetY(y_start + header_heigth)
906 pdf.SetX(left)
906 pdf.SetX(left)
907 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
907 pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
908 left = left + width
908 left = left + width
909 week_f = week_f+7
909 week_f = week_f+7
910 end
910 end
911 end
911 end
912
912
913 # Days headers
913 # Days headers
914 if show_days
914 if show_days
915 left = subject_width
915 left = subject_width
916 height = header_heigth
916 height = header_heigth
917 wday = self.date_from.cwday
917 wday = self.date_from.cwday
918 pdf.SetFontStyle('B',7)
918 pdf.SetFontStyle('B',7)
919 (self.date_to - self.date_from + 1).to_i.times do
919 (self.date_to - self.date_from + 1).to_i.times do
920 width = zoom
920 width = zoom
921 pdf.SetY(y_start + 2 * header_heigth)
921 pdf.SetY(y_start + 2 * header_heigth)
922 pdf.SetX(left)
922 pdf.SetX(left)
923 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
923 pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
924 left = left + width
924 left = left + width
925 wday = wday + 1
925 wday = wday + 1
926 wday = 1 if wday > 7
926 wday = 1 if wday > 7
927 end
927 end
928 end
928 end
929
929
930 pdf.SetY(y_start)
930 pdf.SetY(y_start)
931 pdf.SetX(15)
931 pdf.SetX(15)
932 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
932 pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
933
933
934 # Tasks
934 # Tasks
935 top = headers_heigth + y_start
935 top = headers_heigth + y_start
936 options = {
936 options = {
937 :top => top,
937 :top => top,
938 :zoom => zoom,
938 :zoom => zoom,
939 :subject_width => subject_width,
939 :subject_width => subject_width,
940 :g_width => g_width,
940 :g_width => g_width,
941 :indent => 0,
941 :indent => 0,
942 :indent_increment => 5,
942 :indent_increment => 5,
943 :top_increment => 5,
943 :top_increment => 5,
944 :format => :pdf,
944 :format => :pdf,
945 :pdf => pdf
945 :pdf => pdf
946 }
946 }
947 render(options)
947 render(options)
948 pdf.Output
948 pdf.Output
949 end
949 end
950
950
951 private
951 private
952
952
953 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
953 # Sorts a collection of issues by start_date, due_date, id for gantt rendering
954 def sort_issues!(issues)
954 def sort_issues!(issues)
955 issues.sort! do |a, b|
955 issues.sort! do |a, b|
956 cmp = 0
956 cmp = 0
957 cmp = (a.start_date <=> b.start_date) if a.start_date? && b.start_date?
957 cmp = (a.start_date <=> b.start_date) if a.start_date? && b.start_date?
958 cmp = (a.due_date <=> b.due_date) if cmp == 0 && a.due_date? && b.due_date?
958 cmp = (a.due_date <=> b.due_date) if cmp == 0 && a.due_date? && b.due_date?
959 cmp = (a.id <=> b.id) if cmp == 0
959 cmp = (a.id <=> b.id) if cmp == 0
960 cmp
960 cmp
961 end
961 end
962 end
962 end
963
963
964 def pdf_new_page?(options)
964 def pdf_new_page?(options)
965 if options[:top] > 180
965 if options[:top] > 180
966 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
966 options[:pdf].Line(15, options[:top], PDF::TotalWidth, options[:top])
967 options[:pdf].AddPage("L")
967 options[:pdf].AddPage("L")
968 options[:top] = 15
968 options[:top] = 15
969 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
969 options[:pdf].Line(15, options[:top] - 0.1, PDF::TotalWidth, options[:top] - 0.1)
970 end
970 end
971 end
971 end
972 end
972 end
973 end
973 end
974 end
974 end
@@ -1,1047 +1,1031
1 # redMine - project management software
1 # redMine - project management software
2 # Copyright (C) 2006-2007 Jean-Philippe Lang
2 # Copyright (C) 2006-2007 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.dirname(__FILE__) + '/../test_helper'
18 require File.dirname(__FILE__) + '/../test_helper'
19
19
20 class ProjectTest < ActiveSupport::TestCase
20 class ProjectTest < ActiveSupport::TestCase
21 fixtures :all
21 fixtures :all
22
22
23 def setup
23 def setup
24 @ecookbook = Project.find(1)
24 @ecookbook = Project.find(1)
25 @ecookbook_sub1 = Project.find(3)
25 @ecookbook_sub1 = Project.find(3)
26 User.current = nil
26 User.current = nil
27 end
27 end
28
28
29 should_validate_presence_of :name
29 should_validate_presence_of :name
30 should_validate_presence_of :identifier
30 should_validate_presence_of :identifier
31
31
32 should_validate_uniqueness_of :identifier
32 should_validate_uniqueness_of :identifier
33
33
34 context "associations" do
34 context "associations" do
35 should_have_many :members
35 should_have_many :members
36 should_have_many :users, :through => :members
36 should_have_many :users, :through => :members
37 should_have_many :member_principals
37 should_have_many :member_principals
38 should_have_many :principals, :through => :member_principals
38 should_have_many :principals, :through => :member_principals
39 should_have_many :enabled_modules
39 should_have_many :enabled_modules
40 should_have_many :issues
40 should_have_many :issues
41 should_have_many :issue_changes, :through => :issues
41 should_have_many :issue_changes, :through => :issues
42 should_have_many :versions
42 should_have_many :versions
43 should_have_many :time_entries
43 should_have_many :time_entries
44 should_have_many :queries
44 should_have_many :queries
45 should_have_many :documents
45 should_have_many :documents
46 should_have_many :news
46 should_have_many :news
47 should_have_many :issue_categories
47 should_have_many :issue_categories
48 should_have_many :boards
48 should_have_many :boards
49 should_have_many :changesets, :through => :repository
49 should_have_many :changesets, :through => :repository
50
50
51 should_have_one :repository
51 should_have_one :repository
52 should_have_one :wiki
52 should_have_one :wiki
53
53
54 should_have_and_belong_to_many :trackers
54 should_have_and_belong_to_many :trackers
55 should_have_and_belong_to_many :issue_custom_fields
55 should_have_and_belong_to_many :issue_custom_fields
56 end
56 end
57
57
58 def test_truth
58 def test_truth
59 assert_kind_of Project, @ecookbook
59 assert_kind_of Project, @ecookbook
60 assert_equal "eCookbook", @ecookbook.name
60 assert_equal "eCookbook", @ecookbook.name
61 end
61 end
62
62
63 def test_default_attributes
63 def test_default_attributes
64 with_settings :default_projects_public => '1' do
64 with_settings :default_projects_public => '1' do
65 assert_equal true, Project.new.is_public
65 assert_equal true, Project.new.is_public
66 assert_equal false, Project.new(:is_public => false).is_public
66 assert_equal false, Project.new(:is_public => false).is_public
67 end
67 end
68
68
69 with_settings :default_projects_public => '0' do
69 with_settings :default_projects_public => '0' do
70 assert_equal false, Project.new.is_public
70 assert_equal false, Project.new.is_public
71 assert_equal true, Project.new(:is_public => true).is_public
71 assert_equal true, Project.new(:is_public => true).is_public
72 end
72 end
73
73
74 with_settings :sequential_project_identifiers => '1' do
74 with_settings :sequential_project_identifiers => '1' do
75 assert !Project.new.identifier.blank?
75 assert !Project.new.identifier.blank?
76 assert Project.new(:identifier => '').identifier.blank?
76 assert Project.new(:identifier => '').identifier.blank?
77 end
77 end
78
78
79 with_settings :sequential_project_identifiers => '0' do
79 with_settings :sequential_project_identifiers => '0' do
80 assert Project.new.identifier.blank?
80 assert Project.new.identifier.blank?
81 assert !Project.new(:identifier => 'test').blank?
81 assert !Project.new(:identifier => 'test').blank?
82 end
82 end
83
83
84 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
84 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
85 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
85 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
86 end
86 end
87
87
88 assert_equal Tracker.all, Project.new.trackers
88 assert_equal Tracker.all, Project.new.trackers
89 assert_equal Tracker.find(1, 3), Project.new(:tracker_ids => [1, 3]).trackers
89 assert_equal Tracker.find(1, 3), Project.new(:tracker_ids => [1, 3]).trackers
90 end
90 end
91
91
92 def test_update
92 def test_update
93 assert_equal "eCookbook", @ecookbook.name
93 assert_equal "eCookbook", @ecookbook.name
94 @ecookbook.name = "eCook"
94 @ecookbook.name = "eCook"
95 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
95 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
96 @ecookbook.reload
96 @ecookbook.reload
97 assert_equal "eCook", @ecookbook.name
97 assert_equal "eCook", @ecookbook.name
98 end
98 end
99
99
100 def test_validate_identifier
100 def test_validate_identifier
101 to_test = {"abc" => true,
101 to_test = {"abc" => true,
102 "ab12" => true,
102 "ab12" => true,
103 "ab-12" => true,
103 "ab-12" => true,
104 "12" => false,
104 "12" => false,
105 "new" => false}
105 "new" => false}
106
106
107 to_test.each do |identifier, valid|
107 to_test.each do |identifier, valid|
108 p = Project.new
108 p = Project.new
109 p.identifier = identifier
109 p.identifier = identifier
110 p.valid?
110 p.valid?
111 assert_equal valid, p.errors.on('identifier').nil?
111 assert_equal valid, p.errors.on('identifier').nil?
112 end
112 end
113 end
113 end
114
114
115 def test_members_should_be_active_users
115 def test_members_should_be_active_users
116 Project.all.each do |project|
116 Project.all.each do |project|
117 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
117 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
118 end
118 end
119 end
119 end
120
120
121 def test_users_should_be_active_users
121 def test_users_should_be_active_users
122 Project.all.each do |project|
122 Project.all.each do |project|
123 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
123 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
124 end
124 end
125 end
125 end
126
126
127 def test_archive
127 def test_archive
128 user = @ecookbook.members.first.user
128 user = @ecookbook.members.first.user
129 @ecookbook.archive
129 @ecookbook.archive
130 @ecookbook.reload
130 @ecookbook.reload
131
131
132 assert !@ecookbook.active?
132 assert !@ecookbook.active?
133 assert @ecookbook.archived?
133 assert @ecookbook.archived?
134 assert !user.projects.include?(@ecookbook)
134 assert !user.projects.include?(@ecookbook)
135 # Subproject are also archived
135 # Subproject are also archived
136 assert !@ecookbook.children.empty?
136 assert !@ecookbook.children.empty?
137 assert @ecookbook.descendants.active.empty?
137 assert @ecookbook.descendants.active.empty?
138 end
138 end
139
139
140 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
140 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
141 # Assign an issue of a project to a version of a child project
141 # Assign an issue of a project to a version of a child project
142 Issue.find(4).update_attribute :fixed_version_id, 4
142 Issue.find(4).update_attribute :fixed_version_id, 4
143
143
144 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
144 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
145 assert_equal false, @ecookbook.archive
145 assert_equal false, @ecookbook.archive
146 end
146 end
147 @ecookbook.reload
147 @ecookbook.reload
148 assert @ecookbook.active?
148 assert @ecookbook.active?
149 end
149 end
150
150
151 def test_unarchive
151 def test_unarchive
152 user = @ecookbook.members.first.user
152 user = @ecookbook.members.first.user
153 @ecookbook.archive
153 @ecookbook.archive
154 # A subproject of an archived project can not be unarchived
154 # A subproject of an archived project can not be unarchived
155 assert !@ecookbook_sub1.unarchive
155 assert !@ecookbook_sub1.unarchive
156
156
157 # Unarchive project
157 # Unarchive project
158 assert @ecookbook.unarchive
158 assert @ecookbook.unarchive
159 @ecookbook.reload
159 @ecookbook.reload
160 assert @ecookbook.active?
160 assert @ecookbook.active?
161 assert !@ecookbook.archived?
161 assert !@ecookbook.archived?
162 assert user.projects.include?(@ecookbook)
162 assert user.projects.include?(@ecookbook)
163 # Subproject can now be unarchived
163 # Subproject can now be unarchived
164 @ecookbook_sub1.reload
164 @ecookbook_sub1.reload
165 assert @ecookbook_sub1.unarchive
165 assert @ecookbook_sub1.unarchive
166 end
166 end
167
167
168 def test_destroy
168 def test_destroy
169 # 2 active members
169 # 2 active members
170 assert_equal 2, @ecookbook.members.size
170 assert_equal 2, @ecookbook.members.size
171 # and 1 is locked
171 # and 1 is locked
172 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
172 assert_equal 3, Member.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).size
173 # some boards
173 # some boards
174 assert @ecookbook.boards.any?
174 assert @ecookbook.boards.any?
175
175
176 @ecookbook.destroy
176 @ecookbook.destroy
177 # make sure that the project non longer exists
177 # make sure that the project non longer exists
178 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
178 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
179 # make sure related data was removed
179 # make sure related data was removed
180 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
180 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
181 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
181 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
182 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
182 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
183 end
183 end
184
184
185 def test_move_an_orphan_project_to_a_root_project
185 def test_move_an_orphan_project_to_a_root_project
186 sub = Project.find(2)
186 sub = Project.find(2)
187 sub.set_parent! @ecookbook
187 sub.set_parent! @ecookbook
188 assert_equal @ecookbook.id, sub.parent.id
188 assert_equal @ecookbook.id, sub.parent.id
189 @ecookbook.reload
189 @ecookbook.reload
190 assert_equal 4, @ecookbook.children.size
190 assert_equal 4, @ecookbook.children.size
191 end
191 end
192
192
193 def test_move_an_orphan_project_to_a_subproject
193 def test_move_an_orphan_project_to_a_subproject
194 sub = Project.find(2)
194 sub = Project.find(2)
195 assert sub.set_parent!(@ecookbook_sub1)
195 assert sub.set_parent!(@ecookbook_sub1)
196 end
196 end
197
197
198 def test_move_a_root_project_to_a_project
198 def test_move_a_root_project_to_a_project
199 sub = @ecookbook
199 sub = @ecookbook
200 assert sub.set_parent!(Project.find(2))
200 assert sub.set_parent!(Project.find(2))
201 end
201 end
202
202
203 def test_should_not_move_a_project_to_its_children
203 def test_should_not_move_a_project_to_its_children
204 sub = @ecookbook
204 sub = @ecookbook
205 assert !(sub.set_parent!(Project.find(3)))
205 assert !(sub.set_parent!(Project.find(3)))
206 end
206 end
207
207
208 def test_set_parent_should_add_roots_in_alphabetical_order
208 def test_set_parent_should_add_roots_in_alphabetical_order
209 ProjectCustomField.delete_all
209 ProjectCustomField.delete_all
210 Project.delete_all
210 Project.delete_all
211 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
211 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
212 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
212 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
213 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
213 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
214 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
214 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
215
215
216 assert_equal 4, Project.count
216 assert_equal 4, Project.count
217 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
217 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
218 end
218 end
219
219
220 def test_set_parent_should_add_children_in_alphabetical_order
220 def test_set_parent_should_add_children_in_alphabetical_order
221 ProjectCustomField.delete_all
221 ProjectCustomField.delete_all
222 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
222 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
223 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
223 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
224 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
224 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
225 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
225 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
226 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
226 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
227
227
228 parent.reload
228 parent.reload
229 assert_equal 4, parent.children.size
229 assert_equal 4, parent.children.size
230 assert_equal parent.children.sort_by(&:name), parent.children
230 assert_equal parent.children.sort_by(&:name), parent.children
231 end
231 end
232
232
233 def test_rebuild_should_sort_children_alphabetically
233 def test_rebuild_should_sort_children_alphabetically
234 ProjectCustomField.delete_all
234 ProjectCustomField.delete_all
235 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
235 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
236 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
236 Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
237 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
237 Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
238 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
238 Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
239 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
239 Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
240
240
241 Project.update_all("lft = NULL, rgt = NULL")
241 Project.update_all("lft = NULL, rgt = NULL")
242 Project.rebuild!
242 Project.rebuild!
243
243
244 parent.reload
244 parent.reload
245 assert_equal 4, parent.children.size
245 assert_equal 4, parent.children.size
246 assert_equal parent.children.sort_by(&:name), parent.children
246 assert_equal parent.children.sort_by(&:name), parent.children
247 end
247 end
248
248
249
249
250 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
250 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
251 # Parent issue with a hierarchy project's fixed version
251 # Parent issue with a hierarchy project's fixed version
252 parent_issue = Issue.find(1)
252 parent_issue = Issue.find(1)
253 parent_issue.update_attribute(:fixed_version_id, 4)
253 parent_issue.update_attribute(:fixed_version_id, 4)
254 parent_issue.reload
254 parent_issue.reload
255 assert_equal 4, parent_issue.fixed_version_id
255 assert_equal 4, parent_issue.fixed_version_id
256
256
257 # Should keep fixed versions for the issues
257 # Should keep fixed versions for the issues
258 issue_with_local_fixed_version = Issue.find(5)
258 issue_with_local_fixed_version = Issue.find(5)
259 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
259 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
260 issue_with_local_fixed_version.reload
260 issue_with_local_fixed_version.reload
261 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
261 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
262
262
263 # Local issue with hierarchy fixed_version
263 # Local issue with hierarchy fixed_version
264 issue_with_hierarchy_fixed_version = Issue.find(13)
264 issue_with_hierarchy_fixed_version = Issue.find(13)
265 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
265 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
266 issue_with_hierarchy_fixed_version.reload
266 issue_with_hierarchy_fixed_version.reload
267 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
267 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
268
268
269 # Move project out of the issue's hierarchy
269 # Move project out of the issue's hierarchy
270 moved_project = Project.find(3)
270 moved_project = Project.find(3)
271 moved_project.set_parent!(Project.find(2))
271 moved_project.set_parent!(Project.find(2))
272 parent_issue.reload
272 parent_issue.reload
273 issue_with_local_fixed_version.reload
273 issue_with_local_fixed_version.reload
274 issue_with_hierarchy_fixed_version.reload
274 issue_with_hierarchy_fixed_version.reload
275
275
276 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
276 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
277 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"
277 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"
278 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
278 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
279 end
279 end
280
280
281 def test_parent
281 def test_parent
282 p = Project.find(6).parent
282 p = Project.find(6).parent
283 assert p.is_a?(Project)
283 assert p.is_a?(Project)
284 assert_equal 5, p.id
284 assert_equal 5, p.id
285 end
285 end
286
286
287 def test_ancestors
287 def test_ancestors
288 a = Project.find(6).ancestors
288 a = Project.find(6).ancestors
289 assert a.first.is_a?(Project)
289 assert a.first.is_a?(Project)
290 assert_equal [1, 5], a.collect(&:id)
290 assert_equal [1, 5], a.collect(&:id)
291 end
291 end
292
292
293 def test_root
293 def test_root
294 r = Project.find(6).root
294 r = Project.find(6).root
295 assert r.is_a?(Project)
295 assert r.is_a?(Project)
296 assert_equal 1, r.id
296 assert_equal 1, r.id
297 end
297 end
298
298
299 def test_children
299 def test_children
300 c = Project.find(1).children
300 c = Project.find(1).children
301 assert c.first.is_a?(Project)
301 assert c.first.is_a?(Project)
302 assert_equal [5, 3, 4], c.collect(&:id)
302 assert_equal [5, 3, 4], c.collect(&:id)
303 end
303 end
304
304
305 def test_descendants
305 def test_descendants
306 d = Project.find(1).descendants
306 d = Project.find(1).descendants
307 assert d.first.is_a?(Project)
307 assert d.first.is_a?(Project)
308 assert_equal [5, 6, 3, 4], d.collect(&:id)
308 assert_equal [5, 6, 3, 4], d.collect(&:id)
309 end
309 end
310
310
311 def test_allowed_parents_should_be_empty_for_non_member_user
311 def test_allowed_parents_should_be_empty_for_non_member_user
312 Role.non_member.add_permission!(:add_project)
312 Role.non_member.add_permission!(:add_project)
313 user = User.find(9)
313 user = User.find(9)
314 assert user.memberships.empty?
314 assert user.memberships.empty?
315 User.current = user
315 User.current = user
316 assert Project.new.allowed_parents.compact.empty?
316 assert Project.new.allowed_parents.compact.empty?
317 end
317 end
318
318
319 def test_allowed_parents_with_add_subprojects_permission
319 def test_allowed_parents_with_add_subprojects_permission
320 Role.find(1).remove_permission!(:add_project)
320 Role.find(1).remove_permission!(:add_project)
321 Role.find(1).add_permission!(:add_subprojects)
321 Role.find(1).add_permission!(:add_subprojects)
322 User.current = User.find(2)
322 User.current = User.find(2)
323 # new project
323 # new project
324 assert !Project.new.allowed_parents.include?(nil)
324 assert !Project.new.allowed_parents.include?(nil)
325 assert Project.new.allowed_parents.include?(Project.find(1))
325 assert Project.new.allowed_parents.include?(Project.find(1))
326 # existing root project
326 # existing root project
327 assert Project.find(1).allowed_parents.include?(nil)
327 assert Project.find(1).allowed_parents.include?(nil)
328 # existing child
328 # existing child
329 assert Project.find(3).allowed_parents.include?(Project.find(1))
329 assert Project.find(3).allowed_parents.include?(Project.find(1))
330 assert !Project.find(3).allowed_parents.include?(nil)
330 assert !Project.find(3).allowed_parents.include?(nil)
331 end
331 end
332
332
333 def test_allowed_parents_with_add_project_permission
333 def test_allowed_parents_with_add_project_permission
334 Role.find(1).add_permission!(:add_project)
334 Role.find(1).add_permission!(:add_project)
335 Role.find(1).remove_permission!(:add_subprojects)
335 Role.find(1).remove_permission!(:add_subprojects)
336 User.current = User.find(2)
336 User.current = User.find(2)
337 # new project
337 # new project
338 assert Project.new.allowed_parents.include?(nil)
338 assert Project.new.allowed_parents.include?(nil)
339 assert !Project.new.allowed_parents.include?(Project.find(1))
339 assert !Project.new.allowed_parents.include?(Project.find(1))
340 # existing root project
340 # existing root project
341 assert Project.find(1).allowed_parents.include?(nil)
341 assert Project.find(1).allowed_parents.include?(nil)
342 # existing child
342 # existing child
343 assert Project.find(3).allowed_parents.include?(Project.find(1))
343 assert Project.find(3).allowed_parents.include?(Project.find(1))
344 assert Project.find(3).allowed_parents.include?(nil)
344 assert Project.find(3).allowed_parents.include?(nil)
345 end
345 end
346
346
347 def test_allowed_parents_with_add_project_and_subprojects_permission
347 def test_allowed_parents_with_add_project_and_subprojects_permission
348 Role.find(1).add_permission!(:add_project)
348 Role.find(1).add_permission!(:add_project)
349 Role.find(1).add_permission!(:add_subprojects)
349 Role.find(1).add_permission!(:add_subprojects)
350 User.current = User.find(2)
350 User.current = User.find(2)
351 # new project
351 # new project
352 assert Project.new.allowed_parents.include?(nil)
352 assert Project.new.allowed_parents.include?(nil)
353 assert Project.new.allowed_parents.include?(Project.find(1))
353 assert Project.new.allowed_parents.include?(Project.find(1))
354 # existing root project
354 # existing root project
355 assert Project.find(1).allowed_parents.include?(nil)
355 assert Project.find(1).allowed_parents.include?(nil)
356 # existing child
356 # existing child
357 assert Project.find(3).allowed_parents.include?(Project.find(1))
357 assert Project.find(3).allowed_parents.include?(Project.find(1))
358 assert Project.find(3).allowed_parents.include?(nil)
358 assert Project.find(3).allowed_parents.include?(nil)
359 end
359 end
360
360
361 def test_users_by_role
361 def test_users_by_role
362 users_by_role = Project.find(1).users_by_role
362 users_by_role = Project.find(1).users_by_role
363 assert_kind_of Hash, users_by_role
363 assert_kind_of Hash, users_by_role
364 role = Role.find(1)
364 role = Role.find(1)
365 assert_kind_of Array, users_by_role[role]
365 assert_kind_of Array, users_by_role[role]
366 assert users_by_role[role].include?(User.find(2))
366 assert users_by_role[role].include?(User.find(2))
367 end
367 end
368
368
369 def test_rolled_up_trackers
369 def test_rolled_up_trackers
370 parent = Project.find(1)
370 parent = Project.find(1)
371 parent.trackers = Tracker.find([1,2])
371 parent.trackers = Tracker.find([1,2])
372 child = parent.children.find(3)
372 child = parent.children.find(3)
373
373
374 assert_equal [1, 2], parent.tracker_ids
374 assert_equal [1, 2], parent.tracker_ids
375 assert_equal [2, 3], child.trackers.collect(&:id)
375 assert_equal [2, 3], child.trackers.collect(&:id)
376
376
377 assert_kind_of Tracker, parent.rolled_up_trackers.first
377 assert_kind_of Tracker, parent.rolled_up_trackers.first
378 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
378 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
379
379
380 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
380 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
381 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
381 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
382 end
382 end
383
383
384 def test_rolled_up_trackers_should_ignore_archived_subprojects
384 def test_rolled_up_trackers_should_ignore_archived_subprojects
385 parent = Project.find(1)
385 parent = Project.find(1)
386 parent.trackers = Tracker.find([1,2])
386 parent.trackers = Tracker.find([1,2])
387 child = parent.children.find(3)
387 child = parent.children.find(3)
388 child.trackers = Tracker.find([1,3])
388 child.trackers = Tracker.find([1,3])
389 parent.children.each(&:archive)
389 parent.children.each(&:archive)
390
390
391 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
391 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
392 end
392 end
393
393
394 context "#rolled_up_versions" do
394 context "#rolled_up_versions" do
395 setup do
395 setup do
396 @project = Project.generate!
396 @project = Project.generate!
397 @parent_version_1 = Version.generate!(:project => @project)
397 @parent_version_1 = Version.generate!(:project => @project)
398 @parent_version_2 = Version.generate!(:project => @project)
398 @parent_version_2 = Version.generate!(:project => @project)
399 end
399 end
400
400
401 should "include the versions for the current project" do
401 should "include the versions for the current project" do
402 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
402 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
403 end
403 end
404
404
405 should "include versions for a subproject" do
405 should "include versions for a subproject" do
406 @subproject = Project.generate!
406 @subproject = Project.generate!
407 @subproject.set_parent!(@project)
407 @subproject.set_parent!(@project)
408 @subproject_version = Version.generate!(:project => @subproject)
408 @subproject_version = Version.generate!(:project => @subproject)
409
409
410 assert_same_elements [
410 assert_same_elements [
411 @parent_version_1,
411 @parent_version_1,
412 @parent_version_2,
412 @parent_version_2,
413 @subproject_version
413 @subproject_version
414 ], @project.rolled_up_versions
414 ], @project.rolled_up_versions
415 end
415 end
416
416
417 should "include versions for a sub-subproject" do
417 should "include versions for a sub-subproject" do
418 @subproject = Project.generate!
418 @subproject = Project.generate!
419 @subproject.set_parent!(@project)
419 @subproject.set_parent!(@project)
420 @sub_subproject = Project.generate!
420 @sub_subproject = Project.generate!
421 @sub_subproject.set_parent!(@subproject)
421 @sub_subproject.set_parent!(@subproject)
422 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
422 @sub_subproject_version = Version.generate!(:project => @sub_subproject)
423
423
424 @project.reload
424 @project.reload
425
425
426 assert_same_elements [
426 assert_same_elements [
427 @parent_version_1,
427 @parent_version_1,
428 @parent_version_2,
428 @parent_version_2,
429 @sub_subproject_version
429 @sub_subproject_version
430 ], @project.rolled_up_versions
430 ], @project.rolled_up_versions
431 end
431 end
432
432
433
433
434 should "only check active projects" do
434 should "only check active projects" do
435 @subproject = Project.generate!
435 @subproject = Project.generate!
436 @subproject.set_parent!(@project)
436 @subproject.set_parent!(@project)
437 @subproject_version = Version.generate!(:project => @subproject)
437 @subproject_version = Version.generate!(:project => @subproject)
438 assert @subproject.archive
438 assert @subproject.archive
439
439
440 @project.reload
440 @project.reload
441
441
442 assert !@subproject.active?
442 assert !@subproject.active?
443 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
443 assert_same_elements [@parent_version_1, @parent_version_2], @project.rolled_up_versions
444 end
444 end
445 end
445 end
446
446
447 def test_shared_versions_none_sharing
447 def test_shared_versions_none_sharing
448 p = Project.find(5)
448 p = Project.find(5)
449 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
449 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
450 assert p.shared_versions.include?(v)
450 assert p.shared_versions.include?(v)
451 assert !p.children.first.shared_versions.include?(v)
451 assert !p.children.first.shared_versions.include?(v)
452 assert !p.root.shared_versions.include?(v)
452 assert !p.root.shared_versions.include?(v)
453 assert !p.siblings.first.shared_versions.include?(v)
453 assert !p.siblings.first.shared_versions.include?(v)
454 assert !p.root.siblings.first.shared_versions.include?(v)
454 assert !p.root.siblings.first.shared_versions.include?(v)
455 end
455 end
456
456
457 def test_shared_versions_descendants_sharing
457 def test_shared_versions_descendants_sharing
458 p = Project.find(5)
458 p = Project.find(5)
459 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
459 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
460 assert p.shared_versions.include?(v)
460 assert p.shared_versions.include?(v)
461 assert p.children.first.shared_versions.include?(v)
461 assert p.children.first.shared_versions.include?(v)
462 assert !p.root.shared_versions.include?(v)
462 assert !p.root.shared_versions.include?(v)
463 assert !p.siblings.first.shared_versions.include?(v)
463 assert !p.siblings.first.shared_versions.include?(v)
464 assert !p.root.siblings.first.shared_versions.include?(v)
464 assert !p.root.siblings.first.shared_versions.include?(v)
465 end
465 end
466
466
467 def test_shared_versions_hierarchy_sharing
467 def test_shared_versions_hierarchy_sharing
468 p = Project.find(5)
468 p = Project.find(5)
469 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
469 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
470 assert p.shared_versions.include?(v)
470 assert p.shared_versions.include?(v)
471 assert p.children.first.shared_versions.include?(v)
471 assert p.children.first.shared_versions.include?(v)
472 assert p.root.shared_versions.include?(v)
472 assert p.root.shared_versions.include?(v)
473 assert !p.siblings.first.shared_versions.include?(v)
473 assert !p.siblings.first.shared_versions.include?(v)
474 assert !p.root.siblings.first.shared_versions.include?(v)
474 assert !p.root.siblings.first.shared_versions.include?(v)
475 end
475 end
476
476
477 def test_shared_versions_tree_sharing
477 def test_shared_versions_tree_sharing
478 p = Project.find(5)
478 p = Project.find(5)
479 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
479 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
480 assert p.shared_versions.include?(v)
480 assert p.shared_versions.include?(v)
481 assert p.children.first.shared_versions.include?(v)
481 assert p.children.first.shared_versions.include?(v)
482 assert p.root.shared_versions.include?(v)
482 assert p.root.shared_versions.include?(v)
483 assert p.siblings.first.shared_versions.include?(v)
483 assert p.siblings.first.shared_versions.include?(v)
484 assert !p.root.siblings.first.shared_versions.include?(v)
484 assert !p.root.siblings.first.shared_versions.include?(v)
485 end
485 end
486
486
487 def test_shared_versions_system_sharing
487 def test_shared_versions_system_sharing
488 p = Project.find(5)
488 p = Project.find(5)
489 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
489 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
490 assert p.shared_versions.include?(v)
490 assert p.shared_versions.include?(v)
491 assert p.children.first.shared_versions.include?(v)
491 assert p.children.first.shared_versions.include?(v)
492 assert p.root.shared_versions.include?(v)
492 assert p.root.shared_versions.include?(v)
493 assert p.siblings.first.shared_versions.include?(v)
493 assert p.siblings.first.shared_versions.include?(v)
494 assert p.root.siblings.first.shared_versions.include?(v)
494 assert p.root.siblings.first.shared_versions.include?(v)
495 end
495 end
496
496
497 def test_shared_versions
497 def test_shared_versions
498 parent = Project.find(1)
498 parent = Project.find(1)
499 child = parent.children.find(3)
499 child = parent.children.find(3)
500 private_child = parent.children.find(5)
500 private_child = parent.children.find(5)
501
501
502 assert_equal [1,2,3], parent.version_ids.sort
502 assert_equal [1,2,3], parent.version_ids.sort
503 assert_equal [4], child.version_ids
503 assert_equal [4], child.version_ids
504 assert_equal [6], private_child.version_ids
504 assert_equal [6], private_child.version_ids
505 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
505 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
506
506
507 assert_equal 6, parent.shared_versions.size
507 assert_equal 6, parent.shared_versions.size
508 parent.shared_versions.each do |version|
508 parent.shared_versions.each do |version|
509 assert_kind_of Version, version
509 assert_kind_of Version, version
510 end
510 end
511
511
512 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
512 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
513 end
513 end
514
514
515 def test_shared_versions_should_ignore_archived_subprojects
515 def test_shared_versions_should_ignore_archived_subprojects
516 parent = Project.find(1)
516 parent = Project.find(1)
517 child = parent.children.find(3)
517 child = parent.children.find(3)
518 child.archive
518 child.archive
519 parent.reload
519 parent.reload
520
520
521 assert_equal [1,2,3], parent.version_ids.sort
521 assert_equal [1,2,3], parent.version_ids.sort
522 assert_equal [4], child.version_ids
522 assert_equal [4], child.version_ids
523 assert !parent.shared_versions.collect(&:id).include?(4)
523 assert !parent.shared_versions.collect(&:id).include?(4)
524 end
524 end
525
525
526 def test_shared_versions_visible_to_user
526 def test_shared_versions_visible_to_user
527 user = User.find(3)
527 user = User.find(3)
528 parent = Project.find(1)
528 parent = Project.find(1)
529 child = parent.children.find(5)
529 child = parent.children.find(5)
530
530
531 assert_equal [1,2,3], parent.version_ids.sort
531 assert_equal [1,2,3], parent.version_ids.sort
532 assert_equal [6], child.version_ids
532 assert_equal [6], child.version_ids
533
533
534 versions = parent.shared_versions.visible(user)
534 versions = parent.shared_versions.visible(user)
535
535
536 assert_equal 4, versions.size
536 assert_equal 4, versions.size
537 versions.each do |version|
537 versions.each do |version|
538 assert_kind_of Version, version
538 assert_kind_of Version, version
539 end
539 end
540
540
541 assert !versions.collect(&:id).include?(6)
541 assert !versions.collect(&:id).include?(6)
542 end
542 end
543
543
544
544
545 def test_next_identifier
545 def test_next_identifier
546 ProjectCustomField.delete_all
546 ProjectCustomField.delete_all
547 Project.create!(:name => 'last', :identifier => 'p2008040')
547 Project.create!(:name => 'last', :identifier => 'p2008040')
548 assert_equal 'p2008041', Project.next_identifier
548 assert_equal 'p2008041', Project.next_identifier
549 end
549 end
550
550
551 def test_next_identifier_first_project
551 def test_next_identifier_first_project
552 Project.delete_all
552 Project.delete_all
553 assert_nil Project.next_identifier
553 assert_nil Project.next_identifier
554 end
554 end
555
555
556
556
557 def test_enabled_module_names_should_not_recreate_enabled_modules
557 def test_enabled_module_names_should_not_recreate_enabled_modules
558 project = Project.find(1)
558 project = Project.find(1)
559 # Remove one module
559 # Remove one module
560 modules = project.enabled_modules.slice(0..-2)
560 modules = project.enabled_modules.slice(0..-2)
561 assert modules.any?
561 assert modules.any?
562 assert_difference 'EnabledModule.count', -1 do
562 assert_difference 'EnabledModule.count', -1 do
563 project.enabled_module_names = modules.collect(&:name)
563 project.enabled_module_names = modules.collect(&:name)
564 end
564 end
565 project.reload
565 project.reload
566 # Ids should be preserved
566 # Ids should be preserved
567 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
567 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
568 end
568 end
569
569
570 def test_copy_from_existing_project
570 def test_copy_from_existing_project
571 source_project = Project.find(1)
571 source_project = Project.find(1)
572 copied_project = Project.copy_from(1)
572 copied_project = Project.copy_from(1)
573
573
574 assert copied_project
574 assert copied_project
575 # Cleared attributes
575 # Cleared attributes
576 assert copied_project.id.blank?
576 assert copied_project.id.blank?
577 assert copied_project.name.blank?
577 assert copied_project.name.blank?
578 assert copied_project.identifier.blank?
578 assert copied_project.identifier.blank?
579
579
580 # Duplicated attributes
580 # Duplicated attributes
581 assert_equal source_project.description, copied_project.description
581 assert_equal source_project.description, copied_project.description
582 assert_equal source_project.enabled_modules, copied_project.enabled_modules
582 assert_equal source_project.enabled_modules, copied_project.enabled_modules
583 assert_equal source_project.trackers, copied_project.trackers
583 assert_equal source_project.trackers, copied_project.trackers
584
584
585 # Default attributes
585 # Default attributes
586 assert_equal 1, copied_project.status
586 assert_equal 1, copied_project.status
587 end
587 end
588
588
589 def test_activities_should_use_the_system_activities
589 def test_activities_should_use_the_system_activities
590 project = Project.find(1)
590 project = Project.find(1)
591 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
591 assert_equal project.activities, TimeEntryActivity.find(:all, :conditions => {:active => true} )
592 end
592 end
593
593
594
594
595 def test_activities_should_use_the_project_specific_activities
595 def test_activities_should_use_the_project_specific_activities
596 project = Project.find(1)
596 project = Project.find(1)
597 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
597 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
598 assert overridden_activity.save!
598 assert overridden_activity.save!
599
599
600 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
600 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
601 end
601 end
602
602
603 def test_activities_should_not_include_the_inactive_project_specific_activities
603 def test_activities_should_not_include_the_inactive_project_specific_activities
604 project = Project.find(1)
604 project = Project.find(1)
605 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
605 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
606 assert overridden_activity.save!
606 assert overridden_activity.save!
607
607
608 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
608 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
609 end
609 end
610
610
611 def test_activities_should_not_include_project_specific_activities_from_other_projects
611 def test_activities_should_not_include_project_specific_activities_from_other_projects
612 project = Project.find(1)
612 project = Project.find(1)
613 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
613 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
614 assert overridden_activity.save!
614 assert overridden_activity.save!
615
615
616 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
616 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
617 end
617 end
618
618
619 def test_activities_should_handle_nils
619 def test_activities_should_handle_nils
620 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
620 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.find(:first)})
621 TimeEntryActivity.delete_all
621 TimeEntryActivity.delete_all
622
622
623 # No activities
623 # No activities
624 project = Project.find(1)
624 project = Project.find(1)
625 assert project.activities.empty?
625 assert project.activities.empty?
626
626
627 # No system, one overridden
627 # No system, one overridden
628 assert overridden_activity.save!
628 assert overridden_activity.save!
629 project.reload
629 project.reload
630 assert_equal [overridden_activity], project.activities
630 assert_equal [overridden_activity], project.activities
631 end
631 end
632
632
633 def test_activities_should_override_system_activities_with_project_activities
633 def test_activities_should_override_system_activities_with_project_activities
634 project = Project.find(1)
634 project = Project.find(1)
635 parent_activity = TimeEntryActivity.find(:first)
635 parent_activity = TimeEntryActivity.find(:first)
636 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
636 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
637 assert overridden_activity.save!
637 assert overridden_activity.save!
638
638
639 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
639 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
640 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
640 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
641 end
641 end
642
642
643 def test_activities_should_include_inactive_activities_if_specified
643 def test_activities_should_include_inactive_activities_if_specified
644 project = Project.find(1)
644 project = Project.find(1)
645 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
645 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.find(:first), :active => false})
646 assert overridden_activity.save!
646 assert overridden_activity.save!
647
647
648 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
648 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
649 end
649 end
650
650
651 test 'activities should not include active System activities if the project has an override that is inactive' do
651 test 'activities should not include active System activities if the project has an override that is inactive' do
652 project = Project.find(1)
652 project = Project.find(1)
653 system_activity = TimeEntryActivity.find_by_name('Design')
653 system_activity = TimeEntryActivity.find_by_name('Design')
654 assert system_activity.active?
654 assert system_activity.active?
655 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
655 overridden_activity = TimeEntryActivity.generate!(:project => project, :parent => system_activity, :active => false)
656 assert overridden_activity.save!
656 assert overridden_activity.save!
657
657
658 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
658 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
659 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
659 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
660 end
660 end
661
661
662 def test_close_completed_versions
662 def test_close_completed_versions
663 Version.update_all("status = 'open'")
663 Version.update_all("status = 'open'")
664 project = Project.find(1)
664 project = Project.find(1)
665 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
665 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
666 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
666 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
667 project.close_completed_versions
667 project.close_completed_versions
668 project.reload
668 project.reload
669 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
669 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
670 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
670 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
671 end
671 end
672
672
673 context "Project#copy" do
673 context "Project#copy" do
674 setup do
674 setup do
675 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
675 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
676 Project.destroy_all :identifier => "copy-test"
676 Project.destroy_all :identifier => "copy-test"
677 @source_project = Project.find(2)
677 @source_project = Project.find(2)
678 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
678 @project = Project.new(:name => 'Copy Test', :identifier => 'copy-test')
679 @project.trackers = @source_project.trackers
679 @project.trackers = @source_project.trackers
680 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
680 @project.enabled_module_names = @source_project.enabled_modules.collect(&:name)
681 end
681 end
682
682
683 should "copy issues" do
683 should "copy issues" do
684 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
684 @source_project.issues << Issue.generate!(:status => IssueStatus.find_by_name('Closed'),
685 :subject => "copy issue status",
685 :subject => "copy issue status",
686 :tracker_id => 1,
686 :tracker_id => 1,
687 :assigned_to_id => 2,
687 :assigned_to_id => 2,
688 :project_id => @source_project.id)
688 :project_id => @source_project.id)
689 assert @project.valid?
689 assert @project.valid?
690 assert @project.issues.empty?
690 assert @project.issues.empty?
691 assert @project.copy(@source_project)
691 assert @project.copy(@source_project)
692
692
693 assert_equal @source_project.issues.size, @project.issues.size
693 assert_equal @source_project.issues.size, @project.issues.size
694 @project.issues.each do |issue|
694 @project.issues.each do |issue|
695 assert issue.valid?
695 assert issue.valid?
696 assert ! issue.assigned_to.blank?
696 assert ! issue.assigned_to.blank?
697 assert_equal @project, issue.project
697 assert_equal @project, issue.project
698 end
698 end
699
699
700 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
700 copied_issue = @project.issues.first(:conditions => {:subject => "copy issue status"})
701 assert copied_issue
701 assert copied_issue
702 assert copied_issue.status
702 assert copied_issue.status
703 assert_equal "Closed", copied_issue.status.name
703 assert_equal "Closed", copied_issue.status.name
704 end
704 end
705
705
706 should "change the new issues to use the copied version" do
706 should "change the new issues to use the copied version" do
707 User.current = User.find(1)
707 User.current = User.find(1)
708 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
708 assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
709 @source_project.versions << assigned_version
709 @source_project.versions << assigned_version
710 assert_equal 3, @source_project.versions.size
710 assert_equal 3, @source_project.versions.size
711 Issue.generate_for_project!(@source_project,
711 Issue.generate_for_project!(@source_project,
712 :fixed_version_id => assigned_version.id,
712 :fixed_version_id => assigned_version.id,
713 :subject => "change the new issues to use the copied version",
713 :subject => "change the new issues to use the copied version",
714 :tracker_id => 1,
714 :tracker_id => 1,
715 :project_id => @source_project.id)
715 :project_id => @source_project.id)
716
716
717 assert @project.copy(@source_project)
717 assert @project.copy(@source_project)
718 @project.reload
718 @project.reload
719 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
719 copied_issue = @project.issues.first(:conditions => {:subject => "change the new issues to use the copied version"})
720
720
721 assert copied_issue
721 assert copied_issue
722 assert copied_issue.fixed_version
722 assert copied_issue.fixed_version
723 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
723 assert_equal "Assigned Issues", copied_issue.fixed_version.name # Same name
724 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
724 assert_not_equal assigned_version.id, copied_issue.fixed_version.id # Different record
725 end
725 end
726
726
727 should "copy issue relations" do
727 should "copy issue relations" do
728 Setting.cross_project_issue_relations = '1'
728 Setting.cross_project_issue_relations = '1'
729
729
730 second_issue = Issue.generate!(:status_id => 5,
730 second_issue = Issue.generate!(:status_id => 5,
731 :subject => "copy issue relation",
731 :subject => "copy issue relation",
732 :tracker_id => 1,
732 :tracker_id => 1,
733 :assigned_to_id => 2,
733 :assigned_to_id => 2,
734 :project_id => @source_project.id)
734 :project_id => @source_project.id)
735 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
735 source_relation = IssueRelation.generate!(:issue_from => Issue.find(4),
736 :issue_to => second_issue,
736 :issue_to => second_issue,
737 :relation_type => "relates")
737 :relation_type => "relates")
738 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
738 source_relation_cross_project = IssueRelation.generate!(:issue_from => Issue.find(1),
739 :issue_to => second_issue,
739 :issue_to => second_issue,
740 :relation_type => "duplicates")
740 :relation_type => "duplicates")
741
741
742 assert @project.copy(@source_project)
742 assert @project.copy(@source_project)
743 assert_equal @source_project.issues.count, @project.issues.count
743 assert_equal @source_project.issues.count, @project.issues.count
744 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
744 copied_issue = @project.issues.find_by_subject("Issue on project 2") # Was #4
745 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
745 copied_second_issue = @project.issues.find_by_subject("copy issue relation")
746
746
747 # First issue with a relation on project
747 # First issue with a relation on project
748 assert_equal 1, copied_issue.relations.size, "Relation not copied"
748 assert_equal 1, copied_issue.relations.size, "Relation not copied"
749 copied_relation = copied_issue.relations.first
749 copied_relation = copied_issue.relations.first
750 assert_equal "relates", copied_relation.relation_type
750 assert_equal "relates", copied_relation.relation_type
751 assert_equal copied_second_issue.id, copied_relation.issue_to_id
751 assert_equal copied_second_issue.id, copied_relation.issue_to_id
752 assert_not_equal source_relation.id, copied_relation.id
752 assert_not_equal source_relation.id, copied_relation.id
753
753
754 # Second issue with a cross project relation
754 # Second issue with a cross project relation
755 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
755 assert_equal 2, copied_second_issue.relations.size, "Relation not copied"
756 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
756 copied_relation = copied_second_issue.relations.select {|r| r.relation_type == 'duplicates'}.first
757 assert_equal "duplicates", copied_relation.relation_type
757 assert_equal "duplicates", copied_relation.relation_type
758 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
758 assert_equal 1, copied_relation.issue_from_id, "Cross project relation not kept"
759 assert_not_equal source_relation_cross_project.id, copied_relation.id
759 assert_not_equal source_relation_cross_project.id, copied_relation.id
760 end
760 end
761
761
762 should "copy memberships" do
762 should "copy memberships" do
763 assert @project.valid?
763 assert @project.valid?
764 assert @project.members.empty?
764 assert @project.members.empty?
765 assert @project.copy(@source_project)
765 assert @project.copy(@source_project)
766
766
767 assert_equal @source_project.memberships.size, @project.memberships.size
767 assert_equal @source_project.memberships.size, @project.memberships.size
768 @project.memberships.each do |membership|
768 @project.memberships.each do |membership|
769 assert membership
769 assert membership
770 assert_equal @project, membership.project
770 assert_equal @project, membership.project
771 end
771 end
772 end
772 end
773
773
774 should "copy project specific queries" do
774 should "copy project specific queries" do
775 assert @project.valid?
775 assert @project.valid?
776 assert @project.queries.empty?
776 assert @project.queries.empty?
777 assert @project.copy(@source_project)
777 assert @project.copy(@source_project)
778
778
779 assert_equal @source_project.queries.size, @project.queries.size
779 assert_equal @source_project.queries.size, @project.queries.size
780 @project.queries.each do |query|
780 @project.queries.each do |query|
781 assert query
781 assert query
782 assert_equal @project, query.project
782 assert_equal @project, query.project
783 end
783 end
784 end
784 end
785
785
786 should "copy versions" do
786 should "copy versions" do
787 @source_project.versions << Version.generate!
787 @source_project.versions << Version.generate!
788 @source_project.versions << Version.generate!
788 @source_project.versions << Version.generate!
789
789
790 assert @project.versions.empty?
790 assert @project.versions.empty?
791 assert @project.copy(@source_project)
791 assert @project.copy(@source_project)
792
792
793 assert_equal @source_project.versions.size, @project.versions.size
793 assert_equal @source_project.versions.size, @project.versions.size
794 @project.versions.each do |version|
794 @project.versions.each do |version|
795 assert version
795 assert version
796 assert_equal @project, version.project
796 assert_equal @project, version.project
797 end
797 end
798 end
798 end
799
799
800 should "copy wiki" do
800 should "copy wiki" do
801 assert_difference 'Wiki.count' do
801 assert_difference 'Wiki.count' do
802 assert @project.copy(@source_project)
802 assert @project.copy(@source_project)
803 end
803 end
804
804
805 assert @project.wiki
805 assert @project.wiki
806 assert_not_equal @source_project.wiki, @project.wiki
806 assert_not_equal @source_project.wiki, @project.wiki
807 assert_equal "Start page", @project.wiki.start_page
807 assert_equal "Start page", @project.wiki.start_page
808 end
808 end
809
809
810 should "copy wiki pages and content with hierarchy" do
810 should "copy wiki pages and content with hierarchy" do
811 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
811 assert_difference 'WikiPage.count', @source_project.wiki.pages.size do
812 assert @project.copy(@source_project)
812 assert @project.copy(@source_project)
813 end
813 end
814
814
815 assert @project.wiki
815 assert @project.wiki
816 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
816 assert_equal @source_project.wiki.pages.size, @project.wiki.pages.size
817
817
818 @project.wiki.pages.each do |wiki_page|
818 @project.wiki.pages.each do |wiki_page|
819 assert wiki_page.content
819 assert wiki_page.content
820 assert !@source_project.wiki.pages.include?(wiki_page)
820 assert !@source_project.wiki.pages.include?(wiki_page)
821 end
821 end
822
822
823 parent = @project.wiki.find_page('Parent_page')
823 parent = @project.wiki.find_page('Parent_page')
824 child1 = @project.wiki.find_page('Child_page_1')
824 child1 = @project.wiki.find_page('Child_page_1')
825 child2 = @project.wiki.find_page('Child_page_2')
825 child2 = @project.wiki.find_page('Child_page_2')
826 assert_equal parent, child1.parent
826 assert_equal parent, child1.parent
827 assert_equal parent, child2.parent
827 assert_equal parent, child2.parent
828 end
828 end
829
829
830 should "copy issue categories" do
830 should "copy issue categories" do
831 assert @project.copy(@source_project)
831 assert @project.copy(@source_project)
832
832
833 assert_equal 2, @project.issue_categories.size
833 assert_equal 2, @project.issue_categories.size
834 @project.issue_categories.each do |issue_category|
834 @project.issue_categories.each do |issue_category|
835 assert !@source_project.issue_categories.include?(issue_category)
835 assert !@source_project.issue_categories.include?(issue_category)
836 end
836 end
837 end
837 end
838
838
839 should "copy boards" do
839 should "copy boards" do
840 assert @project.copy(@source_project)
840 assert @project.copy(@source_project)
841
841
842 assert_equal 1, @project.boards.size
842 assert_equal 1, @project.boards.size
843 @project.boards.each do |board|
843 @project.boards.each do |board|
844 assert !@source_project.boards.include?(board)
844 assert !@source_project.boards.include?(board)
845 end
845 end
846 end
846 end
847
847
848 should "change the new issues to use the copied issue categories" do
848 should "change the new issues to use the copied issue categories" do
849 issue = Issue.find(4)
849 issue = Issue.find(4)
850 issue.update_attribute(:category_id, 3)
850 issue.update_attribute(:category_id, 3)
851
851
852 assert @project.copy(@source_project)
852 assert @project.copy(@source_project)
853
853
854 @project.issues.each do |issue|
854 @project.issues.each do |issue|
855 assert issue.category
855 assert issue.category
856 assert_equal "Stock management", issue.category.name # Same name
856 assert_equal "Stock management", issue.category.name # Same name
857 assert_not_equal IssueCategory.find(3), issue.category # Different record
857 assert_not_equal IssueCategory.find(3), issue.category # Different record
858 end
858 end
859 end
859 end
860
860
861 should "limit copy with :only option" do
861 should "limit copy with :only option" do
862 assert @project.members.empty?
862 assert @project.members.empty?
863 assert @project.issue_categories.empty?
863 assert @project.issue_categories.empty?
864 assert @source_project.issues.any?
864 assert @source_project.issues.any?
865
865
866 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
866 assert @project.copy(@source_project, :only => ['members', 'issue_categories'])
867
867
868 assert @project.members.any?
868 assert @project.members.any?
869 assert @project.issue_categories.any?
869 assert @project.issue_categories.any?
870 assert @project.issues.empty?
870 assert @project.issues.empty?
871 end
871 end
872
872
873 end
873 end
874
874
875 context "#start_date" do
875 context "#start_date" do
876 setup do
876 setup do
877 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
877 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
878 @project = Project.generate!(:identifier => 'test0')
878 @project = Project.generate!(:identifier => 'test0')
879 @project.trackers << Tracker.generate!
879 @project.trackers << Tracker.generate!
880 end
880 end
881
881
882 should "be nil if there are no issues on the project" do
882 should "be nil if there are no issues on the project" do
883 assert_nil @project.start_date
883 assert_nil @project.start_date
884 end
884 end
885
886 should "be nil if issue tracking is disabled" do
887 Issue.generate_for_project!(@project, :start_date => Date.today)
888 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
889 @project.reload
890
891 assert_nil @project.start_date
892 end
893
885
894 should "be tested when issues have no start date"
886 should "be tested when issues have no start date"
895
887
896 should "be the earliest start date of it's issues" do
888 should "be the earliest start date of it's issues" do
897 early = 7.days.ago.to_date
889 early = 7.days.ago.to_date
898 Issue.generate_for_project!(@project, :start_date => Date.today)
890 Issue.generate_for_project!(@project, :start_date => Date.today)
899 Issue.generate_for_project!(@project, :start_date => early)
891 Issue.generate_for_project!(@project, :start_date => early)
900
892
901 assert_equal early, @project.start_date
893 assert_equal early, @project.start_date
902 end
894 end
903
895
904 end
896 end
905
897
906 context "#due_date" do
898 context "#due_date" do
907 setup do
899 setup do
908 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
900 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
909 @project = Project.generate!(:identifier => 'test0')
901 @project = Project.generate!(:identifier => 'test0')
910 @project.trackers << Tracker.generate!
902 @project.trackers << Tracker.generate!
911 end
903 end
912
904
913 should "be nil if there are no issues on the project" do
905 should "be nil if there are no issues on the project" do
914 assert_nil @project.due_date
906 assert_nil @project.due_date
915 end
907 end
916
917 should "be nil if issue tracking is disabled" do
918 Issue.generate_for_project!(@project, :due_date => Date.today)
919 @project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
920 @project.reload
921
922 assert_nil @project.due_date
923 end
924
908
925 should "be tested when issues have no due date"
909 should "be tested when issues have no due date"
926
910
927 should "be the latest due date of it's issues" do
911 should "be the latest due date of it's issues" do
928 future = 7.days.from_now.to_date
912 future = 7.days.from_now.to_date
929 Issue.generate_for_project!(@project, :due_date => future)
913 Issue.generate_for_project!(@project, :due_date => future)
930 Issue.generate_for_project!(@project, :due_date => Date.today)
914 Issue.generate_for_project!(@project, :due_date => Date.today)
931
915
932 assert_equal future, @project.due_date
916 assert_equal future, @project.due_date
933 end
917 end
934
918
935 should "be the latest due date of it's versions" do
919 should "be the latest due date of it's versions" do
936 future = 7.days.from_now.to_date
920 future = 7.days.from_now.to_date
937 @project.versions << Version.generate!(:effective_date => future)
921 @project.versions << Version.generate!(:effective_date => future)
938 @project.versions << Version.generate!(:effective_date => Date.today)
922 @project.versions << Version.generate!(:effective_date => Date.today)
939
923
940
924
941 assert_equal future, @project.due_date
925 assert_equal future, @project.due_date
942
926
943 end
927 end
944
928
945 should "pick the latest date from it's issues and versions" do
929 should "pick the latest date from it's issues and versions" do
946 future = 7.days.from_now.to_date
930 future = 7.days.from_now.to_date
947 far_future = 14.days.from_now.to_date
931 far_future = 14.days.from_now.to_date
948 Issue.generate_for_project!(@project, :due_date => far_future)
932 Issue.generate_for_project!(@project, :due_date => far_future)
949 @project.versions << Version.generate!(:effective_date => future)
933 @project.versions << Version.generate!(:effective_date => future)
950
934
951 assert_equal far_future, @project.due_date
935 assert_equal far_future, @project.due_date
952 end
936 end
953
937
954 end
938 end
955
939
956 context "Project#completed_percent" do
940 context "Project#completed_percent" do
957 setup do
941 setup do
958 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
942 ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
959 @project = Project.generate!(:identifier => 'test0')
943 @project = Project.generate!(:identifier => 'test0')
960 @project.trackers << Tracker.generate!
944 @project.trackers << Tracker.generate!
961 end
945 end
962
946
963 context "no versions" do
947 context "no versions" do
964 should "be 100" do
948 should "be 100" do
965 assert_equal 100, @project.completed_percent
949 assert_equal 100, @project.completed_percent
966 end
950 end
967 end
951 end
968
952
969 context "with versions" do
953 context "with versions" do
970 should "return 0 if the versions have no issues" do
954 should "return 0 if the versions have no issues" do
971 Version.generate!(:project => @project)
955 Version.generate!(:project => @project)
972 Version.generate!(:project => @project)
956 Version.generate!(:project => @project)
973
957
974 assert_equal 0, @project.completed_percent
958 assert_equal 0, @project.completed_percent
975 end
959 end
976
960
977 should "return 100 if the version has only closed issues" do
961 should "return 100 if the version has only closed issues" do
978 v1 = Version.generate!(:project => @project)
962 v1 = Version.generate!(:project => @project)
979 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
963 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
980 v2 = Version.generate!(:project => @project)
964 v2 = Version.generate!(:project => @project)
981 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
965 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
982
966
983 assert_equal 100, @project.completed_percent
967 assert_equal 100, @project.completed_percent
984 end
968 end
985
969
986 should "return the averaged completed percent of the versions (not weighted)" do
970 should "return the averaged completed percent of the versions (not weighted)" do
987 v1 = Version.generate!(:project => @project)
971 v1 = Version.generate!(:project => @project)
988 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
972 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
989 v2 = Version.generate!(:project => @project)
973 v2 = Version.generate!(:project => @project)
990 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
974 Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
991
975
992 assert_equal 50, @project.completed_percent
976 assert_equal 50, @project.completed_percent
993 end
977 end
994
978
995 end
979 end
996 end
980 end
997
981
998 context "#notified_users" do
982 context "#notified_users" do
999 setup do
983 setup do
1000 @project = Project.generate!
984 @project = Project.generate!
1001 @role = Role.generate!
985 @role = Role.generate!
1002
986
1003 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
987 @user_with_membership_notification = User.generate!(:mail_notification => 'selected')
1004 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
988 Member.generate!(:project => @project, :roles => [@role], :principal => @user_with_membership_notification, :mail_notification => true)
1005
989
1006 @all_events_user = User.generate!(:mail_notification => 'all')
990 @all_events_user = User.generate!(:mail_notification => 'all')
1007 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
991 Member.generate!(:project => @project, :roles => [@role], :principal => @all_events_user)
1008
992
1009 @no_events_user = User.generate!(:mail_notification => 'none')
993 @no_events_user = User.generate!(:mail_notification => 'none')
1010 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
994 Member.generate!(:project => @project, :roles => [@role], :principal => @no_events_user)
1011
995
1012 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
996 @only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
1013 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
997 Member.generate!(:project => @project, :roles => [@role], :principal => @only_my_events_user)
1014
998
1015 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
999 @only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
1016 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1000 Member.generate!(:project => @project, :roles => [@role], :principal => @only_assigned_user)
1017
1001
1018 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1002 @only_owned_user = User.generate!(:mail_notification => 'only_owner')
1019 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1003 Member.generate!(:project => @project, :roles => [@role], :principal => @only_owned_user)
1020 end
1004 end
1021
1005
1022 should "include members with a mail notification" do
1006 should "include members with a mail notification" do
1023 assert @project.notified_users.include?(@user_with_membership_notification)
1007 assert @project.notified_users.include?(@user_with_membership_notification)
1024 end
1008 end
1025
1009
1026 should "include users with the 'all' notification option" do
1010 should "include users with the 'all' notification option" do
1027 assert @project.notified_users.include?(@all_events_user)
1011 assert @project.notified_users.include?(@all_events_user)
1028 end
1012 end
1029
1013
1030 should "not include users with the 'none' notification option" do
1014 should "not include users with the 'none' notification option" do
1031 assert !@project.notified_users.include?(@no_events_user)
1015 assert !@project.notified_users.include?(@no_events_user)
1032 end
1016 end
1033
1017
1034 should "not include users with the 'only_my_events' notification option" do
1018 should "not include users with the 'only_my_events' notification option" do
1035 assert !@project.notified_users.include?(@only_my_events_user)
1019 assert !@project.notified_users.include?(@only_my_events_user)
1036 end
1020 end
1037
1021
1038 should "not include users with the 'only_assigned' notification option" do
1022 should "not include users with the 'only_assigned' notification option" do
1039 assert !@project.notified_users.include?(@only_assigned_user)
1023 assert !@project.notified_users.include?(@only_assigned_user)
1040 end
1024 end
1041
1025
1042 should "not include users with the 'only_owner' notification option" do
1026 should "not include users with the 'only_owner' notification option" do
1043 assert !@project.notified_users.include?(@only_owned_user)
1027 assert !@project.notified_users.include?(@only_owned_user)
1044 end
1028 end
1045 end
1029 end
1046
1030
1047 end
1031 end
General Comments 0
You need to be logged in to leave comments. Login now