##// END OF EJS Templates
Possibility to define the default enable trackers when creating a project (#13175)....
Jean-Philippe Lang -
r11164:4e9fbeb85165
parent child
Show More
@@ -1,106 +1,106
1 1 # encoding: utf-8
2 2 #
3 3 # Redmine - project management software
4 4 # Copyright (C) 2006-2013 Jean-Philippe Lang
5 5 #
6 6 # This program is free software; you can redistribute it and/or
7 7 # modify it under the terms of the GNU General Public License
8 8 # as published by the Free Software Foundation; either version 2
9 9 # of the License, or (at your option) any later version.
10 10 #
11 11 # This program is distributed in the hope that it will be useful,
12 12 # but WITHOUT ANY WARRANTY; without even the implied warranty of
13 13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
14 14 # GNU General Public License for more details.
15 15 #
16 16 # You should have received a copy of the GNU General Public License
17 17 # along with this program; if not, write to the Free Software
18 18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
19 19
20 20 module SettingsHelper
21 21 def administration_settings_tabs
22 22 tabs = [{:name => 'general', :partial => 'settings/general', :label => :label_general},
23 23 {:name => 'display', :partial => 'settings/display', :label => :label_display},
24 24 {:name => 'authentication', :partial => 'settings/authentication', :label => :label_authentication},
25 25 {:name => 'projects', :partial => 'settings/projects', :label => :label_project_plural},
26 26 {:name => 'issues', :partial => 'settings/issues', :label => :label_issue_tracking},
27 27 {:name => 'notifications', :partial => 'settings/notifications', :label => :field_mail_notification},
28 28 {:name => 'mail_handler', :partial => 'settings/mail_handler', :label => :label_incoming_emails},
29 29 {:name => 'repositories', :partial => 'settings/repositories', :label => :label_repository_plural}
30 30 ]
31 31 end
32 32
33 33 def setting_select(setting, choices, options={})
34 34 if blank_text = options.delete(:blank)
35 35 choices = [[blank_text.is_a?(Symbol) ? l(blank_text) : blank_text, '']] + choices
36 36 end
37 37 setting_label(setting, options).html_safe +
38 38 select_tag("settings[#{setting}]",
39 39 options_for_select(choices, Setting.send(setting).to_s),
40 40 options).html_safe
41 41 end
42 42
43 43 def setting_multiselect(setting, choices, options={})
44 44 setting_values = Setting.send(setting)
45 45 setting_values = [] unless setting_values.is_a?(Array)
46 46
47 47 content_tag("label", l(options[:label] || "setting_#{setting}")) +
48 48 hidden_field_tag("settings[#{setting}][]", '').html_safe +
49 49 choices.collect do |choice|
50 50 text, value = (choice.is_a?(Array) ? choice : [choice, choice])
51 51 content_tag(
52 52 'label',
53 53 check_box_tag(
54 54 "settings[#{setting}][]",
55 55 value,
56 Setting.send(setting).include?(value),
56 setting_values.include?(value),
57 57 :id => nil
58 58 ) + text.to_s,
59 59 :class => (options[:inline] ? 'inline' : 'block')
60 60 )
61 61 end.join.html_safe
62 62 end
63 63
64 64 def setting_text_field(setting, options={})
65 65 setting_label(setting, options).html_safe +
66 66 text_field_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
67 67 end
68 68
69 69 def setting_text_area(setting, options={})
70 70 setting_label(setting, options).html_safe +
71 71 text_area_tag("settings[#{setting}]", Setting.send(setting), options).html_safe
72 72 end
73 73
74 74 def setting_check_box(setting, options={})
75 75 setting_label(setting, options).html_safe +
76 76 hidden_field_tag("settings[#{setting}]", 0, :id => nil).html_safe +
77 77 check_box_tag("settings[#{setting}]", 1, Setting.send("#{setting}?"), options).html_safe
78 78 end
79 79
80 80 def setting_label(setting, options={})
81 81 label = options.delete(:label)
82 82 label != false ? label_tag("settings_#{setting}", l(label || "setting_#{setting}")).html_safe : ''
83 83 end
84 84
85 85 # Renders a notification field for a Redmine::Notifiable option
86 86 def notification_field(notifiable)
87 87 return content_tag(:label,
88 88 check_box_tag('settings[notified_events][]',
89 89 notifiable.name,
90 90 Setting.notified_events.include?(notifiable.name), :id => nil).html_safe +
91 91 l_or_humanize(notifiable.name, :prefix => 'label_').html_safe,
92 92 :class => notifiable.parent.present? ? "parent" : '').html_safe
93 93 end
94 94
95 95 def cross_project_subtasks_options
96 96 options = [
97 97 [:label_disabled, ''],
98 98 [:label_cross_project_system, 'system'],
99 99 [:label_cross_project_tree, 'tree'],
100 100 [:label_cross_project_hierarchy, 'hierarchy'],
101 101 [:label_cross_project_descendants, 'descendants']
102 102 ]
103 103
104 104 options.map {|label, value| [l(label), value.to_s]}
105 105 end
106 106 end
@@ -1,1020 +1,1025
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 class Project < ActiveRecord::Base
19 19 include Redmine::SafeAttributes
20 20
21 21 # Project statuses
22 22 STATUS_ACTIVE = 1
23 23 STATUS_CLOSED = 5
24 24 STATUS_ARCHIVED = 9
25 25
26 26 # Maximum length for project identifiers
27 27 IDENTIFIER_MAX_LENGTH = 100
28 28
29 29 # Specific overidden Activities
30 30 has_many :time_entry_activities
31 31 has_many :members, :include => [:principal, :roles], :conditions => "#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE}"
32 32 has_many :memberships, :class_name => 'Member'
33 33 has_many :member_principals, :class_name => 'Member',
34 34 :include => :principal,
35 35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{Principal::STATUS_ACTIVE})"
36 36 has_many :users, :through => :members
37 37 has_many :principals, :through => :member_principals, :source => :principal
38 38
39 39 has_many :enabled_modules, :dependent => :delete_all
40 40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position"
41 41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker]
42 42 has_many :issue_changes, :through => :issues, :source => :journals
43 43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC"
44 44 has_many :time_entries, :dependent => :delete_all
45 45 has_many :queries, :class_name => 'IssueQuery', :dependent => :delete_all
46 46 has_many :documents, :dependent => :destroy
47 47 has_many :news, :dependent => :destroy, :include => :author
48 48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name"
49 49 has_many :boards, :dependent => :destroy, :order => "position ASC"
50 50 has_one :repository, :conditions => ["is_default = ?", true]
51 51 has_many :repositories, :dependent => :destroy
52 52 has_many :changesets, :through => :repository
53 53 has_one :wiki, :dependent => :destroy
54 54 # Custom field for the project issues
55 55 has_and_belongs_to_many :issue_custom_fields,
56 56 :class_name => 'IssueCustomField',
57 57 :order => "#{CustomField.table_name}.position",
58 58 :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
59 59 :association_foreign_key => 'custom_field_id'
60 60
61 61 acts_as_nested_set :order => 'name', :dependent => :destroy
62 62 acts_as_attachable :view_permission => :view_files,
63 63 :delete_permission => :manage_files
64 64
65 65 acts_as_customizable
66 66 acts_as_searchable :columns => ['name', 'identifier', 'description'], :project_key => 'id', :permission => nil
67 67 acts_as_event :title => Proc.new {|o| "#{l(:label_project)}: #{o.name}"},
68 68 :url => Proc.new {|o| {:controller => 'projects', :action => 'show', :id => o}},
69 69 :author => nil
70 70
71 71 attr_protected :status
72 72
73 73 validates_presence_of :name, :identifier
74 74 validates_uniqueness_of :identifier
75 75 validates_associated :repository, :wiki
76 76 validates_length_of :name, :maximum => 255
77 77 validates_length_of :homepage, :maximum => 255
78 78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH
79 79 # donwcase letters, digits, dashes but not digits only
80 80 validates_format_of :identifier, :with => /\A(?!\d+$)[a-z0-9\-_]*\z/, :if => Proc.new { |p| p.identifier_changed? }
81 81 # reserved words
82 82 validates_exclusion_of :identifier, :in => %w( new )
83 83
84 84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?}
85 85 after_save :update_inherited_members, :if => Proc.new {|project| project.inherit_members_changed?}
86 86 before_destroy :delete_all_members
87 87
88 88 scope :has_module, lambda {|mod|
89 89 where("#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s)
90 90 }
91 91 scope :active, lambda { where(:status => STATUS_ACTIVE) }
92 92 scope :status, lambda {|arg| where(arg.blank? ? nil : {:status => arg.to_i}) }
93 93 scope :all_public, lambda { where(:is_public => true) }
94 94 scope :visible, lambda {|*args| where(Project.visible_condition(args.shift || User.current, *args)) }
95 95 scope :allowed_to, lambda {|*args|
96 96 user = User.current
97 97 permission = nil
98 98 if args.first.is_a?(Symbol)
99 99 permission = args.shift
100 100 else
101 101 user = args.shift
102 102 permission = args.shift
103 103 end
104 104 where(Project.allowed_to_condition(user, permission, *args))
105 105 }
106 106 scope :like, lambda {|arg|
107 107 if arg.blank?
108 108 where(nil)
109 109 else
110 110 pattern = "%#{arg.to_s.strip.downcase}%"
111 111 where("LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", :p => pattern)
112 112 end
113 113 }
114 114
115 115 def initialize(attributes=nil, *args)
116 116 super
117 117
118 118 initialized = (attributes || {}).stringify_keys
119 119 if !initialized.key?('identifier') && Setting.sequential_project_identifiers?
120 120 self.identifier = Project.next_identifier
121 121 end
122 122 if !initialized.key?('is_public')
123 123 self.is_public = Setting.default_projects_public?
124 124 end
125 125 if !initialized.key?('enabled_module_names')
126 126 self.enabled_module_names = Setting.default_projects_modules
127 127 end
128 128 if !initialized.key?('trackers') && !initialized.key?('tracker_ids')
129 default = Setting.default_projects_tracker_ids
130 if default.is_a?(Array)
131 self.trackers = Tracker.where(:id => default.map(&:to_i)).sorted.all
132 else
129 133 self.trackers = Tracker.sorted.all
130 134 end
131 135 end
136 end
132 137
133 138 def identifier=(identifier)
134 139 super unless identifier_frozen?
135 140 end
136 141
137 142 def identifier_frozen?
138 143 errors[:identifier].blank? && !(new_record? || identifier.blank?)
139 144 end
140 145
141 146 # returns latest created projects
142 147 # non public projects will be returned only if user is a member of those
143 148 def self.latest(user=nil, count=5)
144 149 visible(user).limit(count).order("created_on DESC").all
145 150 end
146 151
147 152 # Returns true if the project is visible to +user+ or to the current user.
148 153 def visible?(user=User.current)
149 154 user.allowed_to?(:view_project, self)
150 155 end
151 156
152 157 # Returns a SQL conditions string used to find all projects visible by the specified user.
153 158 #
154 159 # Examples:
155 160 # Project.visible_condition(admin) => "projects.status = 1"
156 161 # Project.visible_condition(normal_user) => "((projects.status = 1) AND (projects.is_public = 1 OR projects.id IN (1,3,4)))"
157 162 # Project.visible_condition(anonymous) => "((projects.status = 1) AND (projects.is_public = 1))"
158 163 def self.visible_condition(user, options={})
159 164 allowed_to_condition(user, :view_project, options)
160 165 end
161 166
162 167 # Returns a SQL conditions string used to find all projects for which +user+ has the given +permission+
163 168 #
164 169 # Valid options:
165 170 # * :project => limit the condition to project
166 171 # * :with_subprojects => limit the condition to project and its subprojects
167 172 # * :member => limit the condition to the user projects
168 173 def self.allowed_to_condition(user, permission, options={})
169 174 perm = Redmine::AccessControl.permission(permission)
170 175 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}")
171 176 if perm && perm.project_module
172 177 # If the permission belongs to a project module, make sure the module is enabled
173 178 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')"
174 179 end
175 180 if options[:project]
176 181 project_statement = "#{Project.table_name}.id = #{options[:project].id}"
177 182 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects]
178 183 base_statement = "(#{project_statement}) AND (#{base_statement})"
179 184 end
180 185
181 186 if user.admin?
182 187 base_statement
183 188 else
184 189 statement_by_role = {}
185 190 unless options[:member]
186 191 role = user.logged? ? Role.non_member : Role.anonymous
187 192 if role.allowed_to?(permission)
188 193 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}"
189 194 end
190 195 end
191 196 if user.logged?
192 197 user.projects_by_role.each do |role, projects|
193 198 if role.allowed_to?(permission) && projects.any?
194 199 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})"
195 200 end
196 201 end
197 202 end
198 203 if statement_by_role.empty?
199 204 "1=0"
200 205 else
201 206 if block_given?
202 207 statement_by_role.each do |role, statement|
203 208 if s = yield(role, user)
204 209 statement_by_role[role] = "(#{statement} AND (#{s}))"
205 210 end
206 211 end
207 212 end
208 213 "((#{base_statement}) AND (#{statement_by_role.values.join(' OR ')}))"
209 214 end
210 215 end
211 216 end
212 217
213 218 # Returns the Systemwide and project specific activities
214 219 def activities(include_inactive=false)
215 220 if include_inactive
216 221 return all_activities
217 222 else
218 223 return active_activities
219 224 end
220 225 end
221 226
222 227 # Will create a new Project specific Activity or update an existing one
223 228 #
224 229 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
225 230 # does not successfully save.
226 231 def update_or_create_time_entry_activity(id, activity_hash)
227 232 if activity_hash.respond_to?(:has_key?) && activity_hash.has_key?('parent_id')
228 233 self.create_time_entry_activity_if_needed(activity_hash)
229 234 else
230 235 activity = project.time_entry_activities.find_by_id(id.to_i)
231 236 activity.update_attributes(activity_hash) if activity
232 237 end
233 238 end
234 239
235 240 # Create a new TimeEntryActivity if it overrides a system TimeEntryActivity
236 241 #
237 242 # This will raise a ActiveRecord::Rollback if the TimeEntryActivity
238 243 # does not successfully save.
239 244 def create_time_entry_activity_if_needed(activity)
240 245 if activity['parent_id']
241 246
242 247 parent_activity = TimeEntryActivity.find(activity['parent_id'])
243 248 activity['name'] = parent_activity.name
244 249 activity['position'] = parent_activity.position
245 250
246 251 if Enumeration.overridding_change?(activity, parent_activity)
247 252 project_activity = self.time_entry_activities.create(activity)
248 253
249 254 if project_activity.new_record?
250 255 raise ActiveRecord::Rollback, "Overridding TimeEntryActivity was not successfully saved"
251 256 else
252 257 self.time_entries.update_all("activity_id = #{project_activity.id}", ["activity_id = ?", parent_activity.id])
253 258 end
254 259 end
255 260 end
256 261 end
257 262
258 263 # Returns a :conditions SQL string that can be used to find the issues associated with this project.
259 264 #
260 265 # Examples:
261 266 # project.project_condition(true) => "(projects.id = 1 OR (projects.lft > 1 AND projects.rgt < 10))"
262 267 # project.project_condition(false) => "projects.id = 1"
263 268 def project_condition(with_subprojects)
264 269 cond = "#{Project.table_name}.id = #{id}"
265 270 cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects
266 271 cond
267 272 end
268 273
269 274 def self.find(*args)
270 275 if args.first && args.first.is_a?(String) && !args.first.match(/^\d*$/)
271 276 project = find_by_identifier(*args)
272 277 raise ActiveRecord::RecordNotFound, "Couldn't find Project with identifier=#{args.first}" if project.nil?
273 278 project
274 279 else
275 280 super
276 281 end
277 282 end
278 283
279 284 def self.find_by_param(*args)
280 285 self.find(*args)
281 286 end
282 287
283 288 def reload(*args)
284 289 @shared_versions = nil
285 290 @rolled_up_versions = nil
286 291 @rolled_up_trackers = nil
287 292 @all_issue_custom_fields = nil
288 293 @all_time_entry_custom_fields = nil
289 294 @to_param = nil
290 295 @allowed_parents = nil
291 296 @allowed_permissions = nil
292 297 @actions_allowed = nil
293 298 @start_date = nil
294 299 @due_date = nil
295 300 super
296 301 end
297 302
298 303 def to_param
299 304 # id is used for projects with a numeric identifier (compatibility)
300 305 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier)
301 306 end
302 307
303 308 def active?
304 309 self.status == STATUS_ACTIVE
305 310 end
306 311
307 312 def archived?
308 313 self.status == STATUS_ARCHIVED
309 314 end
310 315
311 316 # Archives the project and its descendants
312 317 def archive
313 318 # Check that there is no issue of a non descendant project that is assigned
314 319 # to one of the project or descendant versions
315 320 v_ids = self_and_descendants.collect {|p| p.version_ids}.flatten
316 321 if v_ids.any? &&
317 322 Issue.
318 323 includes(:project).
319 324 where("#{Project.table_name}.lft < ? OR #{Project.table_name}.rgt > ?", lft, rgt).
320 325 where("#{Issue.table_name}.fixed_version_id IN (?)", v_ids).
321 326 exists?
322 327 return false
323 328 end
324 329 Project.transaction do
325 330 archive!
326 331 end
327 332 true
328 333 end
329 334
330 335 # Unarchives the project
331 336 # All its ancestors must be active
332 337 def unarchive
333 338 return false if ancestors.detect {|a| !a.active?}
334 339 update_attribute :status, STATUS_ACTIVE
335 340 end
336 341
337 342 def close
338 343 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED
339 344 end
340 345
341 346 def reopen
342 347 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE
343 348 end
344 349
345 350 # Returns an array of projects the project can be moved to
346 351 # by the current user
347 352 def allowed_parents
348 353 return @allowed_parents if @allowed_parents
349 354 @allowed_parents = Project.where(Project.allowed_to_condition(User.current, :add_subprojects)).all
350 355 @allowed_parents = @allowed_parents - self_and_descendants
351 356 if User.current.allowed_to?(:add_project, nil, :global => true) || (!new_record? && parent.nil?)
352 357 @allowed_parents << nil
353 358 end
354 359 unless parent.nil? || @allowed_parents.empty? || @allowed_parents.include?(parent)
355 360 @allowed_parents << parent
356 361 end
357 362 @allowed_parents
358 363 end
359 364
360 365 # Sets the parent of the project with authorization check
361 366 def set_allowed_parent!(p)
362 367 unless p.nil? || p.is_a?(Project)
363 368 if p.to_s.blank?
364 369 p = nil
365 370 else
366 371 p = Project.find_by_id(p)
367 372 return false unless p
368 373 end
369 374 end
370 375 if p.nil?
371 376 if !new_record? && allowed_parents.empty?
372 377 return false
373 378 end
374 379 elsif !allowed_parents.include?(p)
375 380 return false
376 381 end
377 382 set_parent!(p)
378 383 end
379 384
380 385 # Sets the parent of the project
381 386 # Argument can be either a Project, a String, a Fixnum or nil
382 387 def set_parent!(p)
383 388 unless p.nil? || p.is_a?(Project)
384 389 if p.to_s.blank?
385 390 p = nil
386 391 else
387 392 p = Project.find_by_id(p)
388 393 return false unless p
389 394 end
390 395 end
391 396 if p == parent && !p.nil?
392 397 # Nothing to do
393 398 true
394 399 elsif p.nil? || (p.active? && move_possible?(p))
395 400 set_or_update_position_under(p)
396 401 Issue.update_versions_from_hierarchy_change(self)
397 402 true
398 403 else
399 404 # Can not move to the given target
400 405 false
401 406 end
402 407 end
403 408
404 409 # Recalculates all lft and rgt values based on project names
405 410 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid
406 411 # Used in BuildProjectsTree migration
407 412 def self.rebuild_tree!
408 413 transaction do
409 414 update_all "lft = NULL, rgt = NULL"
410 415 rebuild!(false)
411 416 end
412 417 end
413 418
414 419 # Returns an array of the trackers used by the project and its active sub projects
415 420 def rolled_up_trackers
416 421 @rolled_up_trackers ||=
417 422 Tracker.
418 423 joins(:projects).
419 424 joins("JOIN #{EnabledModule.table_name} ON #{EnabledModule.table_name}.project_id = #{Project.table_name}.id AND #{EnabledModule.table_name}.name = 'issue_tracking'").
420 425 select("DISTINCT #{Tracker.table_name}.*").
421 426 where("#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt).
422 427 sorted.
423 428 all
424 429 end
425 430
426 431 # Closes open and locked project versions that are completed
427 432 def close_completed_versions
428 433 Version.transaction do
429 434 versions.where(:status => %w(open locked)).all.each do |version|
430 435 if version.completed?
431 436 version.update_attribute(:status, 'closed')
432 437 end
433 438 end
434 439 end
435 440 end
436 441
437 442 # Returns a scope of the Versions on subprojects
438 443 def rolled_up_versions
439 444 @rolled_up_versions ||=
440 445 Version.scoped(:include => :project,
441 446 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt])
442 447 end
443 448
444 449 # Returns a scope of the Versions used by the project
445 450 def shared_versions
446 451 if new_record?
447 452 Version.scoped(:include => :project,
448 453 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'")
449 454 else
450 455 @shared_versions ||= begin
451 456 r = root? ? self : root
452 457 Version.scoped(:include => :project,
453 458 :conditions => "#{Project.table_name}.id = #{id}" +
454 459 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" +
455 460 " #{Version.table_name}.sharing = 'system'" +
456 461 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" +
457 462 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" +
458 463 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" +
459 464 "))")
460 465 end
461 466 end
462 467 end
463 468
464 469 # Returns a hash of project users grouped by role
465 470 def users_by_role
466 471 members.includes(:user, :roles).all.inject({}) do |h, m|
467 472 m.roles.each do |r|
468 473 h[r] ||= []
469 474 h[r] << m.user
470 475 end
471 476 h
472 477 end
473 478 end
474 479
475 480 # Deletes all project's members
476 481 def delete_all_members
477 482 me, mr = Member.table_name, MemberRole.table_name
478 483 connection.delete("DELETE FROM #{mr} WHERE #{mr}.member_id IN (SELECT #{me}.id FROM #{me} WHERE #{me}.project_id = #{id})")
479 484 Member.delete_all(['project_id = ?', id])
480 485 end
481 486
482 487 # Users/groups issues can be assigned to
483 488 def assignable_users
484 489 assignable = Setting.issue_group_assignment? ? member_principals : members
485 490 assignable.select {|m| m.roles.detect {|role| role.assignable?}}.collect {|m| m.principal}.sort
486 491 end
487 492
488 493 # Returns the mail adresses of users that should be always notified on project events
489 494 def recipients
490 495 notified_users.collect {|user| user.mail}
491 496 end
492 497
493 498 # Returns the users that should be notified on project events
494 499 def notified_users
495 500 # TODO: User part should be extracted to User#notify_about?
496 501 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal}
497 502 end
498 503
499 504 # Returns an array of all custom fields enabled for project issues
500 505 # (explictly associated custom fields and custom fields enabled for all projects)
501 506 def all_issue_custom_fields
502 507 @all_issue_custom_fields ||= (IssueCustomField.for_all + issue_custom_fields).uniq.sort
503 508 end
504 509
505 510 # Returns an array of all custom fields enabled for project time entries
506 511 # (explictly associated custom fields and custom fields enabled for all projects)
507 512 def all_time_entry_custom_fields
508 513 @all_time_entry_custom_fields ||= (TimeEntryCustomField.for_all + time_entry_custom_fields).uniq.sort
509 514 end
510 515
511 516 def project
512 517 self
513 518 end
514 519
515 520 def <=>(project)
516 521 name.downcase <=> project.name.downcase
517 522 end
518 523
519 524 def to_s
520 525 name
521 526 end
522 527
523 528 # Returns a short description of the projects (first lines)
524 529 def short_description(length = 255)
525 530 description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
526 531 end
527 532
528 533 def css_classes
529 534 s = 'project'
530 535 s << ' root' if root?
531 536 s << ' child' if child?
532 537 s << (leaf? ? ' leaf' : ' parent')
533 538 unless active?
534 539 if archived?
535 540 s << ' archived'
536 541 else
537 542 s << ' closed'
538 543 end
539 544 end
540 545 s
541 546 end
542 547
543 548 # The earliest start date of a project, based on it's issues and versions
544 549 def start_date
545 550 @start_date ||= [
546 551 issues.minimum('start_date'),
547 552 shared_versions.minimum('effective_date'),
548 553 Issue.fixed_version(shared_versions).minimum('start_date')
549 554 ].compact.min
550 555 end
551 556
552 557 # The latest due date of an issue or version
553 558 def due_date
554 559 @due_date ||= [
555 560 issues.maximum('due_date'),
556 561 shared_versions.maximum('effective_date'),
557 562 Issue.fixed_version(shared_versions).maximum('due_date')
558 563 ].compact.max
559 564 end
560 565
561 566 def overdue?
562 567 active? && !due_date.nil? && (due_date < Date.today)
563 568 end
564 569
565 570 # Returns the percent completed for this project, based on the
566 571 # progress on it's versions.
567 572 def completed_percent(options={:include_subprojects => false})
568 573 if options.delete(:include_subprojects)
569 574 total = self_and_descendants.collect(&:completed_percent).sum
570 575
571 576 total / self_and_descendants.count
572 577 else
573 578 if versions.count > 0
574 579 total = versions.collect(&:completed_percent).sum
575 580
576 581 total / versions.count
577 582 else
578 583 100
579 584 end
580 585 end
581 586 end
582 587
583 588 # Return true if this project allows to do the specified action.
584 589 # action can be:
585 590 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')
586 591 # * a permission Symbol (eg. :edit_project)
587 592 def allows_to?(action)
588 593 if archived?
589 594 # No action allowed on archived projects
590 595 return false
591 596 end
592 597 unless active? || Redmine::AccessControl.read_action?(action)
593 598 # No write action allowed on closed projects
594 599 return false
595 600 end
596 601 # No action allowed on disabled modules
597 602 if action.is_a? Hash
598 603 allowed_actions.include? "#{action[:controller]}/#{action[:action]}"
599 604 else
600 605 allowed_permissions.include? action
601 606 end
602 607 end
603 608
604 609 def module_enabled?(module_name)
605 610 module_name = module_name.to_s
606 611 enabled_modules.detect {|m| m.name == module_name}
607 612 end
608 613
609 614 def enabled_module_names=(module_names)
610 615 if module_names && module_names.is_a?(Array)
611 616 module_names = module_names.collect(&:to_s).reject(&:blank?)
612 617 self.enabled_modules = module_names.collect {|name| enabled_modules.detect {|mod| mod.name == name} || EnabledModule.new(:name => name)}
613 618 else
614 619 enabled_modules.clear
615 620 end
616 621 end
617 622
618 623 # Returns an array of the enabled modules names
619 624 def enabled_module_names
620 625 enabled_modules.collect(&:name)
621 626 end
622 627
623 628 # Enable a specific module
624 629 #
625 630 # Examples:
626 631 # project.enable_module!(:issue_tracking)
627 632 # project.enable_module!("issue_tracking")
628 633 def enable_module!(name)
629 634 enabled_modules << EnabledModule.new(:name => name.to_s) unless module_enabled?(name)
630 635 end
631 636
632 637 # Disable a module if it exists
633 638 #
634 639 # Examples:
635 640 # project.disable_module!(:issue_tracking)
636 641 # project.disable_module!("issue_tracking")
637 642 # project.disable_module!(project.enabled_modules.first)
638 643 def disable_module!(target)
639 644 target = enabled_modules.detect{|mod| target.to_s == mod.name} unless enabled_modules.include?(target)
640 645 target.destroy unless target.blank?
641 646 end
642 647
643 648 safe_attributes 'name',
644 649 'description',
645 650 'homepage',
646 651 'is_public',
647 652 'identifier',
648 653 'custom_field_values',
649 654 'custom_fields',
650 655 'tracker_ids',
651 656 'issue_custom_field_ids'
652 657
653 658 safe_attributes 'enabled_module_names',
654 659 :if => lambda {|project, user| project.new_record? || user.allowed_to?(:select_project_modules, project) }
655 660
656 661 safe_attributes 'inherit_members',
657 662 :if => lambda {|project, user| project.parent.nil? || project.parent.visible?(user)}
658 663
659 664 # Returns an array of projects that are in this project's hierarchy
660 665 #
661 666 # Example: parents, children, siblings
662 667 def hierarchy
663 668 parents = project.self_and_ancestors || []
664 669 descendants = project.descendants || []
665 670 project_hierarchy = parents | descendants # Set union
666 671 end
667 672
668 673 # Returns an auto-generated project identifier based on the last identifier used
669 674 def self.next_identifier
670 675 p = Project.order('created_on DESC').first
671 676 p.nil? ? nil : p.identifier.to_s.succ
672 677 end
673 678
674 679 # Copies and saves the Project instance based on the +project+.
675 680 # Duplicates the source project's:
676 681 # * Wiki
677 682 # * Versions
678 683 # * Categories
679 684 # * Issues
680 685 # * Members
681 686 # * Queries
682 687 #
683 688 # Accepts an +options+ argument to specify what to copy
684 689 #
685 690 # Examples:
686 691 # project.copy(1) # => copies everything
687 692 # project.copy(1, :only => 'members') # => copies members only
688 693 # project.copy(1, :only => ['members', 'versions']) # => copies members and versions
689 694 def copy(project, options={})
690 695 project = project.is_a?(Project) ? project : Project.find(project)
691 696
692 697 to_be_copied = %w(wiki versions issue_categories issues members queries boards)
693 698 to_be_copied = to_be_copied & options[:only].to_a unless options[:only].nil?
694 699
695 700 Project.transaction do
696 701 if save
697 702 reload
698 703 to_be_copied.each do |name|
699 704 send "copy_#{name}", project
700 705 end
701 706 Redmine::Hook.call_hook(:model_project_copy_before_save, :source_project => project, :destination_project => self)
702 707 save
703 708 end
704 709 end
705 710 end
706 711
707 712 # Returns a new unsaved Project instance with attributes copied from +project+
708 713 def self.copy_from(project)
709 714 project = project.is_a?(Project) ? project : Project.find(project)
710 715 # clear unique attributes
711 716 attributes = project.attributes.dup.except('id', 'name', 'identifier', 'status', 'parent_id', 'lft', 'rgt')
712 717 copy = Project.new(attributes)
713 718 copy.enabled_modules = project.enabled_modules
714 719 copy.trackers = project.trackers
715 720 copy.custom_values = project.custom_values.collect {|v| v.clone}
716 721 copy.issue_custom_fields = project.issue_custom_fields
717 722 copy
718 723 end
719 724
720 725 # Yields the given block for each project with its level in the tree
721 726 def self.project_tree(projects, &block)
722 727 ancestors = []
723 728 projects.sort_by(&:lft).each do |project|
724 729 while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
725 730 ancestors.pop
726 731 end
727 732 yield project, ancestors.size
728 733 ancestors << project
729 734 end
730 735 end
731 736
732 737 private
733 738
734 739 def after_parent_changed(parent_was)
735 740 remove_inherited_member_roles
736 741 add_inherited_member_roles
737 742 end
738 743
739 744 def update_inherited_members
740 745 if parent
741 746 if inherit_members? && !inherit_members_was
742 747 remove_inherited_member_roles
743 748 add_inherited_member_roles
744 749 elsif !inherit_members? && inherit_members_was
745 750 remove_inherited_member_roles
746 751 end
747 752 end
748 753 end
749 754
750 755 def remove_inherited_member_roles
751 756 member_roles = memberships.map(&:member_roles).flatten
752 757 member_role_ids = member_roles.map(&:id)
753 758 member_roles.each do |member_role|
754 759 if member_role.inherited_from && !member_role_ids.include?(member_role.inherited_from)
755 760 member_role.destroy
756 761 end
757 762 end
758 763 end
759 764
760 765 def add_inherited_member_roles
761 766 if inherit_members? && parent
762 767 parent.memberships.each do |parent_member|
763 768 member = Member.find_or_new(self.id, parent_member.user_id)
764 769 parent_member.member_roles.each do |parent_member_role|
765 770 member.member_roles << MemberRole.new(:role => parent_member_role.role, :inherited_from => parent_member_role.id)
766 771 end
767 772 member.save!
768 773 end
769 774 end
770 775 end
771 776
772 777 # Copies wiki from +project+
773 778 def copy_wiki(project)
774 779 # Check that the source project has a wiki first
775 780 unless project.wiki.nil?
776 781 wiki = self.wiki || Wiki.new
777 782 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id")
778 783 wiki_pages_map = {}
779 784 project.wiki.pages.each do |page|
780 785 # Skip pages without content
781 786 next if page.content.nil?
782 787 new_wiki_content = WikiContent.new(page.content.attributes.dup.except("id", "page_id", "updated_on"))
783 788 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id"))
784 789 new_wiki_page.content = new_wiki_content
785 790 wiki.pages << new_wiki_page
786 791 wiki_pages_map[page.id] = new_wiki_page
787 792 end
788 793
789 794 self.wiki = wiki
790 795 wiki.save
791 796 # Reproduce page hierarchy
792 797 project.wiki.pages.each do |page|
793 798 if page.parent_id && wiki_pages_map[page.id]
794 799 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id]
795 800 wiki_pages_map[page.id].save
796 801 end
797 802 end
798 803 end
799 804 end
800 805
801 806 # Copies versions from +project+
802 807 def copy_versions(project)
803 808 project.versions.each do |version|
804 809 new_version = Version.new
805 810 new_version.attributes = version.attributes.dup.except("id", "project_id", "created_on", "updated_on")
806 811 self.versions << new_version
807 812 end
808 813 end
809 814
810 815 # Copies issue categories from +project+
811 816 def copy_issue_categories(project)
812 817 project.issue_categories.each do |issue_category|
813 818 new_issue_category = IssueCategory.new
814 819 new_issue_category.attributes = issue_category.attributes.dup.except("id", "project_id")
815 820 self.issue_categories << new_issue_category
816 821 end
817 822 end
818 823
819 824 # Copies issues from +project+
820 825 def copy_issues(project)
821 826 # Stores the source issue id as a key and the copied issues as the
822 827 # value. Used to map the two togeather for issue relations.
823 828 issues_map = {}
824 829
825 830 # Store status and reopen locked/closed versions
826 831 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]}
827 832 version_statuses.each do |version, status|
828 833 version.update_attribute :status, 'open'
829 834 end
830 835
831 836 # Get issues sorted by root_id, lft so that parent issues
832 837 # get copied before their children
833 838 project.issues.reorder('root_id, lft').all.each do |issue|
834 839 new_issue = Issue.new
835 840 new_issue.copy_from(issue, :subtasks => false, :link => false)
836 841 new_issue.project = self
837 842 # Reassign fixed_versions by name, since names are unique per project
838 843 if issue.fixed_version && issue.fixed_version.project == project
839 844 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name}
840 845 end
841 846 # Reassign the category by name, since names are unique per project
842 847 if issue.category
843 848 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name}
844 849 end
845 850 # Parent issue
846 851 if issue.parent_id
847 852 if copied_parent = issues_map[issue.parent_id]
848 853 new_issue.parent_issue_id = copied_parent.id
849 854 end
850 855 end
851 856
852 857 self.issues << new_issue
853 858 if new_issue.new_record?
854 859 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info
855 860 else
856 861 issues_map[issue.id] = new_issue unless new_issue.new_record?
857 862 end
858 863 end
859 864
860 865 # Restore locked/closed version statuses
861 866 version_statuses.each do |version, status|
862 867 version.update_attribute :status, status
863 868 end
864 869
865 870 # Relations after in case issues related each other
866 871 project.issues.each do |issue|
867 872 new_issue = issues_map[issue.id]
868 873 unless new_issue
869 874 # Issue was not copied
870 875 next
871 876 end
872 877
873 878 # Relations
874 879 issue.relations_from.each do |source_relation|
875 880 new_issue_relation = IssueRelation.new
876 881 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
877 882 new_issue_relation.issue_to = issues_map[source_relation.issue_to_id]
878 883 if new_issue_relation.issue_to.nil? && Setting.cross_project_issue_relations?
879 884 new_issue_relation.issue_to = source_relation.issue_to
880 885 end
881 886 new_issue.relations_from << new_issue_relation
882 887 end
883 888
884 889 issue.relations_to.each do |source_relation|
885 890 new_issue_relation = IssueRelation.new
886 891 new_issue_relation.attributes = source_relation.attributes.dup.except("id", "issue_from_id", "issue_to_id")
887 892 new_issue_relation.issue_from = issues_map[source_relation.issue_from_id]
888 893 if new_issue_relation.issue_from.nil? && Setting.cross_project_issue_relations?
889 894 new_issue_relation.issue_from = source_relation.issue_from
890 895 end
891 896 new_issue.relations_to << new_issue_relation
892 897 end
893 898 end
894 899 end
895 900
896 901 # Copies members from +project+
897 902 def copy_members(project)
898 903 # Copy users first, then groups to handle members with inherited and given roles
899 904 members_to_copy = []
900 905 members_to_copy += project.memberships.select {|m| m.principal.is_a?(User)}
901 906 members_to_copy += project.memberships.select {|m| !m.principal.is_a?(User)}
902 907
903 908 members_to_copy.each do |member|
904 909 new_member = Member.new
905 910 new_member.attributes = member.attributes.dup.except("id", "project_id", "created_on")
906 911 # only copy non inherited roles
907 912 # inherited roles will be added when copying the group membership
908 913 role_ids = member.member_roles.reject(&:inherited?).collect(&:role_id)
909 914 next if role_ids.empty?
910 915 new_member.role_ids = role_ids
911 916 new_member.project = self
912 917 self.members << new_member
913 918 end
914 919 end
915 920
916 921 # Copies queries from +project+
917 922 def copy_queries(project)
918 923 project.queries.each do |query|
919 924 new_query = IssueQuery.new
920 925 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria")
921 926 new_query.sort_criteria = query.sort_criteria if query.sort_criteria
922 927 new_query.project = self
923 928 new_query.user_id = query.user_id
924 929 self.queries << new_query
925 930 end
926 931 end
927 932
928 933 # Copies boards from +project+
929 934 def copy_boards(project)
930 935 project.boards.each do |board|
931 936 new_board = Board.new
932 937 new_board.attributes = board.attributes.dup.except("id", "project_id", "topics_count", "messages_count", "last_message_id")
933 938 new_board.project = self
934 939 self.boards << new_board
935 940 end
936 941 end
937 942
938 943 def allowed_permissions
939 944 @allowed_permissions ||= begin
940 945 module_names = enabled_modules.all(:select => :name).collect {|m| m.name}
941 946 Redmine::AccessControl.modules_permissions(module_names).collect {|p| p.name}
942 947 end
943 948 end
944 949
945 950 def allowed_actions
946 951 @actions_allowed ||= allowed_permissions.inject([]) { |actions, permission| actions += Redmine::AccessControl.allowed_actions(permission) }.flatten
947 952 end
948 953
949 954 # Returns all the active Systemwide and project specific activities
950 955 def active_activities
951 956 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
952 957
953 958 if overridden_activity_ids.empty?
954 959 return TimeEntryActivity.shared.active
955 960 else
956 961 return system_activities_and_project_overrides
957 962 end
958 963 end
959 964
960 965 # Returns all the Systemwide and project specific activities
961 966 # (inactive and active)
962 967 def all_activities
963 968 overridden_activity_ids = self.time_entry_activities.collect(&:parent_id)
964 969
965 970 if overridden_activity_ids.empty?
966 971 return TimeEntryActivity.shared
967 972 else
968 973 return system_activities_and_project_overrides(true)
969 974 end
970 975 end
971 976
972 977 # Returns the systemwide active activities merged with the project specific overrides
973 978 def system_activities_and_project_overrides(include_inactive=false)
974 979 if include_inactive
975 980 return TimeEntryActivity.shared.
976 981 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
977 982 self.time_entry_activities
978 983 else
979 984 return TimeEntryActivity.shared.active.
980 985 where("id NOT IN (?)", self.time_entry_activities.collect(&:parent_id)).all +
981 986 self.time_entry_activities.active
982 987 end
983 988 end
984 989
985 990 # Archives subprojects recursively
986 991 def archive!
987 992 children.each do |subproject|
988 993 subproject.send :archive!
989 994 end
990 995 update_attribute :status, STATUS_ARCHIVED
991 996 end
992 997
993 998 def update_position_under_parent
994 999 set_or_update_position_under(parent)
995 1000 end
996 1001
997 1002 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted
998 1003 def set_or_update_position_under(target_parent)
999 1004 parent_was = parent
1000 1005 sibs = (target_parent.nil? ? self.class.roots : target_parent.children)
1001 1006 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase }
1002 1007
1003 1008 if to_be_inserted_before
1004 1009 move_to_left_of(to_be_inserted_before)
1005 1010 elsif target_parent.nil?
1006 1011 if sibs.empty?
1007 1012 # move_to_root adds the project in first (ie. left) position
1008 1013 move_to_root
1009 1014 else
1010 1015 move_to_right_of(sibs.last) unless self == sibs.last
1011 1016 end
1012 1017 else
1013 1018 # move_to_child_of adds the project in last (ie.right) position
1014 1019 move_to_child_of(target_parent)
1015 1020 end
1016 1021 if parent_was != target_parent
1017 1022 after_parent_changed(parent_was)
1018 1023 end
1019 1024 end
1020 1025 end
@@ -1,17 +1,20
1 1 <%= form_tag({:action => 'edit', :tab => 'projects'}) do %>
2 2
3 3 <div class="box tabular settings">
4 4 <p><%= setting_check_box :default_projects_public %></p>
5 5
6 6 <p><%= setting_multiselect(:default_projects_modules,
7 7 Redmine::AccessControl.available_project_modules.collect {|m| [l_or_humanize(m, :prefix => "project_module_"), m.to_s]}) %></p>
8 8
9 <p><%= setting_multiselect(:default_projects_tracker_ids,
10 Tracker.sorted.all.collect {|t| [t.name, t.id.to_s]}) %></p>
11
9 12 <p><%= setting_check_box :sequential_project_identifiers %></p>
10 13
11 14 <p><%= setting_select :new_project_user_role_id,
12 15 Role.find_all_givable.collect {|r| [r.name, r.id.to_s]},
13 16 :blank => "--- #{l(:actionview_instancetag_blank_option)} ---" %></p>
14 17 </div>
15 18
16 19 <%= submit_tag l(:button_save) %>
17 20 <% end %>
@@ -1,229 +1,232
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18
19 19 # DO NOT MODIFY THIS FILE !!!
20 20 # Settings can be defined through the application in Admin -> Settings
21 21
22 22 app_title:
23 23 default: Redmine
24 24 app_subtitle:
25 25 default: Project management
26 26 welcome_text:
27 27 default:
28 28 login_required:
29 29 default: 0
30 30 self_registration:
31 31 default: '2'
32 32 lost_password:
33 33 default: 1
34 34 unsubscribe:
35 35 default: 1
36 36 password_min_length:
37 37 format: int
38 38 default: 8
39 39 # Maximum lifetime of user sessions in minutes
40 40 session_lifetime:
41 41 format: int
42 42 default: 0
43 43 # User session timeout in minutes
44 44 session_timeout:
45 45 format: int
46 46 default: 0
47 47 attachment_max_size:
48 48 format: int
49 49 default: 5120
50 50 issues_export_limit:
51 51 format: int
52 52 default: 500
53 53 activity_days_default:
54 54 format: int
55 55 default: 30
56 56 per_page_options:
57 57 default: '25,50,100'
58 58 mail_from:
59 59 default: redmine@example.net
60 60 bcc_recipients:
61 61 default: 1
62 62 plain_text_mail:
63 63 default: 0
64 64 text_formatting:
65 65 default: textile
66 66 cache_formatted_text:
67 67 default: 0
68 68 wiki_compression:
69 69 default: ""
70 70 default_language:
71 71 default: en
72 72 host_name:
73 73 default: localhost:3000
74 74 protocol:
75 75 default: http
76 76 feeds_limit:
77 77 format: int
78 78 default: 15
79 79 gantt_items_limit:
80 80 format: int
81 81 default: 500
82 82 # Maximum size of files that can be displayed
83 83 # inline through the file viewer (in KB)
84 84 file_max_size_displayed:
85 85 format: int
86 86 default: 512
87 87 diff_max_lines_displayed:
88 88 format: int
89 89 default: 1500
90 90 enabled_scm:
91 91 serialized: true
92 92 default:
93 93 - Subversion
94 94 - Darcs
95 95 - Mercurial
96 96 - Cvs
97 97 - Bazaar
98 98 - Git
99 99 autofetch_changesets:
100 100 default: 1
101 101 sys_api_enabled:
102 102 default: 0
103 103 sys_api_key:
104 104 default: ''
105 105 commit_cross_project_ref:
106 106 default: 0
107 107 commit_ref_keywords:
108 108 default: 'refs,references,IssueID'
109 109 commit_fix_keywords:
110 110 default: 'fixes,closes'
111 111 commit_fix_status_id:
112 112 format: int
113 113 default: 0
114 114 commit_fix_done_ratio:
115 115 default: 100
116 116 commit_logtime_enabled:
117 117 default: 0
118 118 commit_logtime_activity_id:
119 119 format: int
120 120 default: 0
121 121 # autologin duration in days
122 122 # 0 means autologin is disabled
123 123 autologin:
124 124 format: int
125 125 default: 0
126 126 # date format
127 127 date_format:
128 128 default: ''
129 129 time_format:
130 130 default: ''
131 131 user_format:
132 132 default: :firstname_lastname
133 133 format: symbol
134 134 cross_project_issue_relations:
135 135 default: 0
136 136 # Enables subtasks to be in other projects
137 137 cross_project_subtasks:
138 138 default: 'tree'
139 139 issue_group_assignment:
140 140 default: 0
141 141 default_issue_start_date_to_creation_date:
142 142 default: 1
143 143 notified_events:
144 144 serialized: true
145 145 default:
146 146 - issue_added
147 147 - issue_updated
148 148 mail_handler_body_delimiters:
149 149 default: ''
150 150 mail_handler_api_enabled:
151 151 default: 0
152 152 mail_handler_api_key:
153 153 default:
154 154 issue_list_default_columns:
155 155 serialized: true
156 156 default:
157 157 - tracker
158 158 - status
159 159 - priority
160 160 - subject
161 161 - assigned_to
162 162 - updated_on
163 163 display_subprojects_issues:
164 164 default: 1
165 165 issue_done_ratio:
166 166 default: 'issue_field'
167 167 default_projects_public:
168 168 default: 1
169 169 default_projects_modules:
170 170 serialized: true
171 171 default:
172 172 - issue_tracking
173 173 - time_tracking
174 174 - news
175 175 - documents
176 176 - files
177 177 - wiki
178 178 - repository
179 179 - boards
180 180 - calendar
181 181 - gantt
182 default_projects_tracker_ids:
183 serialized: true
184 default:
182 185 # Role given to a non-admin user who creates a project
183 186 new_project_user_role_id:
184 187 format: int
185 188 default: ''
186 189 sequential_project_identifiers:
187 190 default: 0
188 191 # encodings used to convert repository files content to UTF-8
189 192 # multiple values accepted, comma separated
190 193 repositories_encodings:
191 194 default: ''
192 195 # encoding used to convert commit logs to UTF-8
193 196 commit_logs_encoding:
194 197 default: 'UTF-8'
195 198 repository_log_display_limit:
196 199 format: int
197 200 default: 100
198 201 ui_theme:
199 202 default: ''
200 203 emails_footer:
201 204 default: |-
202 205 You have received this notification because you have either subscribed to it, or are involved in it.
203 206 To change your notification preferences, please click here: http://hostname/my/account
204 207 gravatar_enabled:
205 208 default: 0
206 209 openid:
207 210 default: 0
208 211 gravatar_default:
209 212 default: ''
210 213 start_of_week:
211 214 default: ''
212 215 rest_api_enabled:
213 216 default: 0
214 217 jsonp_enabled:
215 218 default: 0
216 219 default_notification_option:
217 220 default: 'only_my_events'
218 221 emails_header:
219 222 default: ''
220 223 thumbnails_enabled:
221 224 default: 0
222 225 thumbnails_size:
223 226 format: int
224 227 default: 100
225 228 non_working_week_days:
226 229 serialized: true
227 230 default:
228 231 - '6'
229 232 - '7'
@@ -1,460 +1,468
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 #require 'shoulda'
19 19 ENV["RAILS_ENV"] = "test"
20 20 require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
21 21 require 'rails/test_help'
22 22 require Rails.root.join('test', 'mocks', 'open_id_authentication_mock.rb').to_s
23 23
24 24 require File.expand_path(File.dirname(__FILE__) + '/object_helpers')
25 25 include ObjectHelpers
26 26
27 27 class ActiveSupport::TestCase
28 28 include ActionDispatch::TestProcess
29 29
30 30 self.use_transactional_fixtures = true
31 31 self.use_instantiated_fixtures = false
32 32
33 33 def log_user(login, password)
34 34 User.anonymous
35 35 get "/login"
36 36 assert_equal nil, session[:user_id]
37 37 assert_response :success
38 38 assert_template "account/login"
39 39 post "/login", :username => login, :password => password
40 40 assert_equal login, User.find(session[:user_id]).login
41 41 end
42 42
43 43 def uploaded_test_file(name, mime)
44 44 fixture_file_upload("files/#{name}", mime, true)
45 45 end
46 46
47 47 def credentials(user, password=nil)
48 48 {'HTTP_AUTHORIZATION' => ActionController::HttpAuthentication::Basic.encode_credentials(user, password || user)}
49 49 end
50 50
51 51 # Mock out a file
52 52 def self.mock_file
53 53 file = 'a_file.png'
54 54 file.stubs(:size).returns(32)
55 55 file.stubs(:original_filename).returns('a_file.png')
56 56 file.stubs(:content_type).returns('image/png')
57 57 file.stubs(:read).returns(false)
58 58 file
59 59 end
60 60
61 61 def mock_file
62 62 self.class.mock_file
63 63 end
64 64
65 65 def mock_file_with_options(options={})
66 66 file = ''
67 67 file.stubs(:size).returns(32)
68 68 original_filename = options[:original_filename] || nil
69 69 file.stubs(:original_filename).returns(original_filename)
70 70 content_type = options[:content_type] || nil
71 71 file.stubs(:content_type).returns(content_type)
72 72 file.stubs(:read).returns(false)
73 73 file
74 74 end
75 75
76 76 # Use a temporary directory for attachment related tests
77 77 def set_tmp_attachments_directory
78 78 Dir.mkdir "#{Rails.root}/tmp/test" unless File.directory?("#{Rails.root}/tmp/test")
79 79 unless File.directory?("#{Rails.root}/tmp/test/attachments")
80 80 Dir.mkdir "#{Rails.root}/tmp/test/attachments"
81 81 end
82 82 Attachment.storage_path = "#{Rails.root}/tmp/test/attachments"
83 83 end
84 84
85 85 def set_fixtures_attachments_directory
86 86 Attachment.storage_path = "#{Rails.root}/test/fixtures/files"
87 87 end
88 88
89 89 def with_settings(options, &block)
90 saved_settings = options.keys.inject({}) {|h, k| h[k] = Setting[k].is_a?(Symbol) ? Setting[k] : Setting[k].dup; h}
90 saved_settings = options.keys.inject({}) do |h, k|
91 h[k] = case Setting[k]
92 when Symbol, false, true, nil
93 Setting[k]
94 else
95 Setting[k].dup
96 end
97 h
98 end
91 99 options.each {|k, v| Setting[k] = v}
92 100 yield
93 101 ensure
94 102 saved_settings.each {|k, v| Setting[k] = v} if saved_settings
95 103 end
96 104
97 105 # Yields the block with user as the current user
98 106 def with_current_user(user, &block)
99 107 saved_user = User.current
100 108 User.current = user
101 109 yield
102 110 ensure
103 111 User.current = saved_user
104 112 end
105 113
106 114 def change_user_password(login, new_password)
107 115 user = User.first(:conditions => {:login => login})
108 116 user.password, user.password_confirmation = new_password, new_password
109 117 user.save!
110 118 end
111 119
112 120 def self.ldap_configured?
113 121 @test_ldap = Net::LDAP.new(:host => '127.0.0.1', :port => 389)
114 122 return @test_ldap.bind
115 123 rescue Exception => e
116 124 # LDAP is not listening
117 125 return nil
118 126 end
119 127
120 128 def self.convert_installed?
121 129 Redmine::Thumbnail.convert_available?
122 130 end
123 131
124 132 # Returns the path to the test +vendor+ repository
125 133 def self.repository_path(vendor)
126 134 Rails.root.join("tmp/test/#{vendor.downcase}_repository").to_s
127 135 end
128 136
129 137 # Returns the url of the subversion test repository
130 138 def self.subversion_repository_url
131 139 path = repository_path('subversion')
132 140 path = '/' + path unless path.starts_with?('/')
133 141 "file://#{path}"
134 142 end
135 143
136 144 # Returns true if the +vendor+ test repository is configured
137 145 def self.repository_configured?(vendor)
138 146 File.directory?(repository_path(vendor))
139 147 end
140 148
141 149 def repository_path_hash(arr)
142 150 hs = {}
143 151 hs[:path] = arr.join("/")
144 152 hs[:param] = arr.join("/")
145 153 hs
146 154 end
147 155
148 156 def assert_save(object)
149 157 saved = object.save
150 158 message = "#{object.class} could not be saved"
151 159 errors = object.errors.full_messages.map {|m| "- #{m}"}
152 160 message << ":\n#{errors.join("\n")}" if errors.any?
153 161 assert_equal true, saved, message
154 162 end
155 163
156 164 def assert_error_tag(options={})
157 165 assert_tag({:attributes => { :id => 'errorExplanation' }}.merge(options))
158 166 end
159 167
160 168 def assert_include(expected, s, message=nil)
161 169 assert s.include?(expected), (message || "\"#{expected}\" not found in \"#{s}\"")
162 170 end
163 171
164 172 def assert_not_include(expected, s)
165 173 assert !s.include?(expected), "\"#{expected}\" found in \"#{s}\""
166 174 end
167 175
168 176 def assert_select_in(text, *args, &block)
169 177 d = HTML::Document.new(CGI::unescapeHTML(String.new(text))).root
170 178 assert_select(d, *args, &block)
171 179 end
172 180
173 181 def assert_mail_body_match(expected, mail)
174 182 if expected.is_a?(String)
175 183 assert_include expected, mail_body(mail)
176 184 else
177 185 assert_match expected, mail_body(mail)
178 186 end
179 187 end
180 188
181 189 def assert_mail_body_no_match(expected, mail)
182 190 if expected.is_a?(String)
183 191 assert_not_include expected, mail_body(mail)
184 192 else
185 193 assert_no_match expected, mail_body(mail)
186 194 end
187 195 end
188 196
189 197 def mail_body(mail)
190 198 mail.parts.first.body.encoded
191 199 end
192 200 end
193 201
194 202 module Redmine
195 203 module ApiTest
196 204 # Base class for API tests
197 205 class Base < ActionDispatch::IntegrationTest
198 206 # Test that a request allows the three types of API authentication
199 207 #
200 208 # * HTTP Basic with username and password
201 209 # * HTTP Basic with an api key for the username
202 210 # * Key based with the key=X parameter
203 211 #
204 212 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
205 213 # @param [String] url the request url
206 214 # @param [optional, Hash] parameters additional request parameters
207 215 # @param [optional, Hash] options additional options
208 216 # @option options [Symbol] :success_code Successful response code (:success)
209 217 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
210 218 def self.should_allow_api_authentication(http_method, url, parameters={}, options={})
211 219 should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters, options)
212 220 should_allow_http_basic_auth_with_key(http_method, url, parameters, options)
213 221 should_allow_key_based_auth(http_method, url, parameters, options)
214 222 end
215 223
216 224 # Test that a request allows the username and password for HTTP BASIC
217 225 #
218 226 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
219 227 # @param [String] url the request url
220 228 # @param [optional, Hash] parameters additional request parameters
221 229 # @param [optional, Hash] options additional options
222 230 # @option options [Symbol] :success_code Successful response code (:success)
223 231 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
224 232 def self.should_allow_http_basic_auth_with_username_and_password(http_method, url, parameters={}, options={})
225 233 success_code = options[:success_code] || :success
226 234 failure_code = options[:failure_code] || :unauthorized
227 235
228 236 context "should allow http basic auth using a username and password for #{http_method} #{url}" do
229 237 context "with a valid HTTP authentication" do
230 238 setup do
231 239 @user = User.generate! do |user|
232 240 user.admin = true
233 241 user.password = 'my_password'
234 242 end
235 243 send(http_method, url, parameters, credentials(@user.login, 'my_password'))
236 244 end
237 245
238 246 should_respond_with success_code
239 247 should_respond_with_content_type_based_on_url(url)
240 248 should "login as the user" do
241 249 assert_equal @user, User.current
242 250 end
243 251 end
244 252
245 253 context "with an invalid HTTP authentication" do
246 254 setup do
247 255 @user = User.generate!
248 256 send(http_method, url, parameters, credentials(@user.login, 'wrong_password'))
249 257 end
250 258
251 259 should_respond_with failure_code
252 260 should_respond_with_content_type_based_on_url(url)
253 261 should "not login as the user" do
254 262 assert_equal User.anonymous, User.current
255 263 end
256 264 end
257 265
258 266 context "without credentials" do
259 267 setup do
260 268 send(http_method, url, parameters)
261 269 end
262 270
263 271 should_respond_with failure_code
264 272 should_respond_with_content_type_based_on_url(url)
265 273 should "include_www_authenticate_header" do
266 274 assert @controller.response.headers.has_key?('WWW-Authenticate')
267 275 end
268 276 end
269 277 end
270 278 end
271 279
272 280 # Test that a request allows the API key with HTTP BASIC
273 281 #
274 282 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
275 283 # @param [String] url the request url
276 284 # @param [optional, Hash] parameters additional request parameters
277 285 # @param [optional, Hash] options additional options
278 286 # @option options [Symbol] :success_code Successful response code (:success)
279 287 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
280 288 def self.should_allow_http_basic_auth_with_key(http_method, url, parameters={}, options={})
281 289 success_code = options[:success_code] || :success
282 290 failure_code = options[:failure_code] || :unauthorized
283 291
284 292 context "should allow http basic auth with a key for #{http_method} #{url}" do
285 293 context "with a valid HTTP authentication using the API token" do
286 294 setup do
287 295 @user = User.generate! do |user|
288 296 user.admin = true
289 297 end
290 298 @token = Token.create!(:user => @user, :action => 'api')
291 299 send(http_method, url, parameters, credentials(@token.value, 'X'))
292 300 end
293 301 should_respond_with success_code
294 302 should_respond_with_content_type_based_on_url(url)
295 303 should_be_a_valid_response_string_based_on_url(url)
296 304 should "login as the user" do
297 305 assert_equal @user, User.current
298 306 end
299 307 end
300 308
301 309 context "with an invalid HTTP authentication" do
302 310 setup do
303 311 @user = User.generate!
304 312 @token = Token.create!(:user => @user, :action => 'feeds')
305 313 send(http_method, url, parameters, credentials(@token.value, 'X'))
306 314 end
307 315 should_respond_with failure_code
308 316 should_respond_with_content_type_based_on_url(url)
309 317 should "not login as the user" do
310 318 assert_equal User.anonymous, User.current
311 319 end
312 320 end
313 321 end
314 322 end
315 323
316 324 # Test that a request allows full key authentication
317 325 #
318 326 # @param [Symbol] http_method the HTTP method for request (:get, :post, :put, :delete)
319 327 # @param [String] url the request url, without the key=ZXY parameter
320 328 # @param [optional, Hash] parameters additional request parameters
321 329 # @param [optional, Hash] options additional options
322 330 # @option options [Symbol] :success_code Successful response code (:success)
323 331 # @option options [Symbol] :failure_code Failure response code (:unauthorized)
324 332 def self.should_allow_key_based_auth(http_method, url, parameters={}, options={})
325 333 success_code = options[:success_code] || :success
326 334 failure_code = options[:failure_code] || :unauthorized
327 335
328 336 context "should allow key based auth using key=X for #{http_method} #{url}" do
329 337 context "with a valid api token" do
330 338 setup do
331 339 @user = User.generate! do |user|
332 340 user.admin = true
333 341 end
334 342 @token = Token.create!(:user => @user, :action => 'api')
335 343 # Simple url parse to add on ?key= or &key=
336 344 request_url = if url.match(/\?/)
337 345 url + "&key=#{@token.value}"
338 346 else
339 347 url + "?key=#{@token.value}"
340 348 end
341 349 send(http_method, request_url, parameters)
342 350 end
343 351 should_respond_with success_code
344 352 should_respond_with_content_type_based_on_url(url)
345 353 should_be_a_valid_response_string_based_on_url(url)
346 354 should "login as the user" do
347 355 assert_equal @user, User.current
348 356 end
349 357 end
350 358
351 359 context "with an invalid api token" do
352 360 setup do
353 361 @user = User.generate! do |user|
354 362 user.admin = true
355 363 end
356 364 @token = Token.create!(:user => @user, :action => 'feeds')
357 365 # Simple url parse to add on ?key= or &key=
358 366 request_url = if url.match(/\?/)
359 367 url + "&key=#{@token.value}"
360 368 else
361 369 url + "?key=#{@token.value}"
362 370 end
363 371 send(http_method, request_url, parameters)
364 372 end
365 373 should_respond_with failure_code
366 374 should_respond_with_content_type_based_on_url(url)
367 375 should "not login as the user" do
368 376 assert_equal User.anonymous, User.current
369 377 end
370 378 end
371 379 end
372 380
373 381 context "should allow key based auth using X-Redmine-API-Key header for #{http_method} #{url}" do
374 382 setup do
375 383 @user = User.generate! do |user|
376 384 user.admin = true
377 385 end
378 386 @token = Token.create!(:user => @user, :action => 'api')
379 387 send(http_method, url, parameters, {'X-Redmine-API-Key' => @token.value.to_s})
380 388 end
381 389 should_respond_with success_code
382 390 should_respond_with_content_type_based_on_url(url)
383 391 should_be_a_valid_response_string_based_on_url(url)
384 392 should "login as the user" do
385 393 assert_equal @user, User.current
386 394 end
387 395 end
388 396 end
389 397
390 398 # Uses should_respond_with_content_type based on what's in the url:
391 399 #
392 400 # '/project/issues.xml' => should_respond_with_content_type :xml
393 401 # '/project/issues.json' => should_respond_with_content_type :json
394 402 #
395 403 # @param [String] url Request
396 404 def self.should_respond_with_content_type_based_on_url(url)
397 405 case
398 406 when url.match(/xml/i)
399 407 should "respond with XML" do
400 408 assert_equal 'application/xml', @response.content_type
401 409 end
402 410 when url.match(/json/i)
403 411 should "respond with JSON" do
404 412 assert_equal 'application/json', @response.content_type
405 413 end
406 414 else
407 415 raise "Unknown content type for should_respond_with_content_type_based_on_url: #{url}"
408 416 end
409 417 end
410 418
411 419 # Uses the url to assert which format the response should be in
412 420 #
413 421 # '/project/issues.xml' => should_be_a_valid_xml_string
414 422 # '/project/issues.json' => should_be_a_valid_json_string
415 423 #
416 424 # @param [String] url Request
417 425 def self.should_be_a_valid_response_string_based_on_url(url)
418 426 case
419 427 when url.match(/xml/i)
420 428 should_be_a_valid_xml_string
421 429 when url.match(/json/i)
422 430 should_be_a_valid_json_string
423 431 else
424 432 raise "Unknown content type for should_be_a_valid_response_based_on_url: #{url}"
425 433 end
426 434 end
427 435
428 436 # Checks that the response is a valid JSON string
429 437 def self.should_be_a_valid_json_string
430 438 should "be a valid JSON string (or empty)" do
431 439 assert(response.body.blank? || ActiveSupport::JSON.decode(response.body))
432 440 end
433 441 end
434 442
435 443 # Checks that the response is a valid XML string
436 444 def self.should_be_a_valid_xml_string
437 445 should "be a valid XML string" do
438 446 assert REXML::Document.new(response.body)
439 447 end
440 448 end
441 449
442 450 def self.should_respond_with(status)
443 451 should "respond with #{status}" do
444 452 assert_response status
445 453 end
446 454 end
447 455 end
448 456 end
449 457 end
450 458
451 459 # URL helpers do not work with config.threadsafe!
452 460 # https://github.com/rspec/rspec-rails/issues/476#issuecomment-4705454
453 461 ActionView::TestCase::TestController.instance_eval do
454 462 helper Rails.application.routes.url_helpers
455 463 end
456 464 ActionView::TestCase::TestController.class_eval do
457 465 def _routes
458 466 Rails.application.routes
459 467 end
460 468 end
@@ -1,916 +1,937
1 1 # Redmine - project management software
2 2 # Copyright (C) 2006-2013 Jean-Philippe Lang
3 3 #
4 4 # This program is free software; you can redistribute it and/or
5 5 # modify it under the terms of the GNU General Public License
6 6 # as published by the Free Software Foundation; either version 2
7 7 # of the License, or (at your option) any later version.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU General Public License
15 15 # along with this program; if not, write to the Free Software
16 16 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
17 17
18 18 require File.expand_path('../../test_helper', __FILE__)
19 19
20 20 class ProjectTest < ActiveSupport::TestCase
21 21 fixtures :projects, :trackers, :issue_statuses, :issues,
22 22 :journals, :journal_details,
23 23 :enumerations, :users, :issue_categories,
24 24 :projects_trackers,
25 25 :custom_fields,
26 26 :custom_fields_projects,
27 27 :custom_fields_trackers,
28 28 :custom_values,
29 29 :roles,
30 30 :member_roles,
31 31 :members,
32 32 :enabled_modules,
33 33 :versions,
34 34 :wikis, :wiki_pages, :wiki_contents, :wiki_content_versions,
35 35 :groups_users,
36 36 :boards, :messages,
37 37 :repositories,
38 38 :news, :comments,
39 39 :documents
40 40
41 41 def setup
42 42 @ecookbook = Project.find(1)
43 43 @ecookbook_sub1 = Project.find(3)
44 44 set_tmp_attachments_directory
45 45 User.current = nil
46 46 end
47 47
48 48 def test_truth
49 49 assert_kind_of Project, @ecookbook
50 50 assert_equal "eCookbook", @ecookbook.name
51 51 end
52 52
53 53 def test_default_attributes
54 54 with_settings :default_projects_public => '1' do
55 55 assert_equal true, Project.new.is_public
56 56 assert_equal false, Project.new(:is_public => false).is_public
57 57 end
58 58
59 59 with_settings :default_projects_public => '0' do
60 60 assert_equal false, Project.new.is_public
61 61 assert_equal true, Project.new(:is_public => true).is_public
62 62 end
63 63
64 64 with_settings :sequential_project_identifiers => '1' do
65 65 assert !Project.new.identifier.blank?
66 66 assert Project.new(:identifier => '').identifier.blank?
67 67 end
68 68
69 69 with_settings :sequential_project_identifiers => '0' do
70 70 assert Project.new.identifier.blank?
71 71 assert !Project.new(:identifier => 'test').blank?
72 72 end
73 73
74 74 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
75 75 assert_equal ['issue_tracking', 'repository'], Project.new.enabled_module_names
76 76 end
77 end
78
79 def test_default_trackers_should_match_default_tracker_ids_setting
80 with_settings :default_projects_tracker_ids => ['1', '3'] do
81 assert_equal Tracker.find(1, 3).sort, Project.new.trackers.sort
82 end
83 end
77 84
85 def test_default_trackers_should_be_all_trackers_with_blank_setting
86 with_settings :default_projects_tracker_ids => nil do
78 87 assert_equal Tracker.all.sort, Project.new.trackers.sort
79 assert_equal Tracker.find(1, 3).sort, Project.new(:tracker_ids => [1, 3]).trackers.sort
88 end
89 end
90
91 def test_default_trackers_should_be_empty_with_empty_setting
92 with_settings :default_projects_tracker_ids => [] do
93 assert_equal [], Project.new.trackers
94 end
95 end
96
97 def test_default_trackers_should_not_replace_initialized_trackers
98 with_settings :default_projects_tracker_ids => ['1', '3'] do
99 assert_equal Tracker.find(1, 2).sort, Project.new(:tracker_ids => [1, 2]).trackers.sort
100 end
80 101 end
81 102
82 103 def test_update
83 104 assert_equal "eCookbook", @ecookbook.name
84 105 @ecookbook.name = "eCook"
85 106 assert @ecookbook.save, @ecookbook.errors.full_messages.join("; ")
86 107 @ecookbook.reload
87 108 assert_equal "eCook", @ecookbook.name
88 109 end
89 110
90 111 def test_validate_identifier
91 112 to_test = {"abc" => true,
92 113 "ab12" => true,
93 114 "ab-12" => true,
94 115 "ab_12" => true,
95 116 "12" => false,
96 117 "new" => false}
97 118
98 119 to_test.each do |identifier, valid|
99 120 p = Project.new
100 121 p.identifier = identifier
101 122 p.valid?
102 123 if valid
103 124 assert p.errors['identifier'].blank?, "identifier #{identifier} was not valid"
104 125 else
105 126 assert p.errors['identifier'].present?, "identifier #{identifier} was valid"
106 127 end
107 128 end
108 129 end
109 130
110 131 def test_identifier_should_not_be_frozen_for_a_new_project
111 132 assert_equal false, Project.new.identifier_frozen?
112 133 end
113 134
114 135 def test_identifier_should_not_be_frozen_for_a_saved_project_with_blank_identifier
115 136 Project.update_all(["identifier = ''"], "id = 1")
116 137
117 138 assert_equal false, Project.find(1).identifier_frozen?
118 139 end
119 140
120 141 def test_identifier_should_be_frozen_for_a_saved_project_with_valid_identifier
121 142 assert_equal true, Project.find(1).identifier_frozen?
122 143 end
123 144
124 145 def test_members_should_be_active_users
125 146 Project.all.each do |project|
126 147 assert_nil project.members.detect {|m| !(m.user.is_a?(User) && m.user.active?) }
127 148 end
128 149 end
129 150
130 151 def test_users_should_be_active_users
131 152 Project.all.each do |project|
132 153 assert_nil project.users.detect {|u| !(u.is_a?(User) && u.active?) }
133 154 end
134 155 end
135 156
136 157 def test_open_scope_on_issues_association
137 158 assert_kind_of Issue, Project.find(1).issues.open.first
138 159 end
139 160
140 161 def test_archive
141 162 user = @ecookbook.members.first.user
142 163 @ecookbook.archive
143 164 @ecookbook.reload
144 165
145 166 assert !@ecookbook.active?
146 167 assert @ecookbook.archived?
147 168 assert !user.projects.include?(@ecookbook)
148 169 # Subproject are also archived
149 170 assert !@ecookbook.children.empty?
150 171 assert @ecookbook.descendants.active.empty?
151 172 end
152 173
153 174 def test_archive_should_fail_if_versions_are_used_by_non_descendant_projects
154 175 # Assign an issue of a project to a version of a child project
155 176 Issue.find(4).update_attribute :fixed_version_id, 4
156 177
157 178 assert_no_difference "Project.count(:all, :conditions => 'status = #{Project::STATUS_ARCHIVED}')" do
158 179 assert_equal false, @ecookbook.archive
159 180 end
160 181 @ecookbook.reload
161 182 assert @ecookbook.active?
162 183 end
163 184
164 185 def test_unarchive
165 186 user = @ecookbook.members.first.user
166 187 @ecookbook.archive
167 188 # A subproject of an archived project can not be unarchived
168 189 assert !@ecookbook_sub1.unarchive
169 190
170 191 # Unarchive project
171 192 assert @ecookbook.unarchive
172 193 @ecookbook.reload
173 194 assert @ecookbook.active?
174 195 assert !@ecookbook.archived?
175 196 assert user.projects.include?(@ecookbook)
176 197 # Subproject can now be unarchived
177 198 @ecookbook_sub1.reload
178 199 assert @ecookbook_sub1.unarchive
179 200 end
180 201
181 202 def test_destroy
182 203 # 2 active members
183 204 assert_equal 2, @ecookbook.members.size
184 205 # and 1 is locked
185 206 assert_equal 3, Member.where('project_id = ?', @ecookbook.id).all.size
186 207 # some boards
187 208 assert @ecookbook.boards.any?
188 209
189 210 @ecookbook.destroy
190 211 # make sure that the project non longer exists
191 212 assert_raise(ActiveRecord::RecordNotFound) { Project.find(@ecookbook.id) }
192 213 # make sure related data was removed
193 214 assert_nil Member.first(:conditions => {:project_id => @ecookbook.id})
194 215 assert_nil Board.first(:conditions => {:project_id => @ecookbook.id})
195 216 assert_nil Issue.first(:conditions => {:project_id => @ecookbook.id})
196 217 end
197 218
198 219 def test_destroy_should_destroy_subtasks
199 220 issues = (0..2).to_a.map {Issue.create!(:project_id => 1, :tracker_id => 1, :author_id => 1, :subject => 'test')}
200 221 issues[0].update_attribute :parent_issue_id, issues[1].id
201 222 issues[2].update_attribute :parent_issue_id, issues[1].id
202 223 assert_equal 2, issues[1].children.count
203 224
204 225 assert_nothing_raised do
205 226 Project.find(1).destroy
206 227 end
207 228 assert Issue.find_all_by_id(issues.map(&:id)).empty?
208 229 end
209 230
210 231 def test_destroying_root_projects_should_clear_data
211 232 Project.roots.each do |root|
212 233 root.destroy
213 234 end
214 235
215 236 assert_equal 0, Project.count, "Projects were not deleted: #{Project.all.inspect}"
216 237 assert_equal 0, Member.count, "Members were not deleted: #{Member.all.inspect}"
217 238 assert_equal 0, MemberRole.count
218 239 assert_equal 0, Issue.count
219 240 assert_equal 0, Journal.count
220 241 assert_equal 0, JournalDetail.count
221 242 assert_equal 0, Attachment.count, "Attachments were not deleted: #{Attachment.all.inspect}"
222 243 assert_equal 0, EnabledModule.count
223 244 assert_equal 0, IssueCategory.count
224 245 assert_equal 0, IssueRelation.count
225 246 assert_equal 0, Board.count
226 247 assert_equal 0, Message.count
227 248 assert_equal 0, News.count
228 249 assert_equal 0, Query.count(:conditions => "project_id IS NOT NULL")
229 250 assert_equal 0, Repository.count
230 251 assert_equal 0, Changeset.count
231 252 assert_equal 0, Change.count
232 253 assert_equal 0, Comment.count
233 254 assert_equal 0, TimeEntry.count
234 255 assert_equal 0, Version.count
235 256 assert_equal 0, Watcher.count
236 257 assert_equal 0, Wiki.count
237 258 assert_equal 0, WikiPage.count
238 259 assert_equal 0, WikiContent.count
239 260 assert_equal 0, WikiContent::Version.count
240 261 assert_equal 0, Project.connection.select_all("SELECT * FROM projects_trackers").size
241 262 assert_equal 0, Project.connection.select_all("SELECT * FROM custom_fields_projects").size
242 263 assert_equal 0, CustomValue.count(:conditions => {:customized_type => ['Project', 'Issue', 'TimeEntry', 'Version']})
243 264 end
244 265
245 266 def test_move_an_orphan_project_to_a_root_project
246 267 sub = Project.find(2)
247 268 sub.set_parent! @ecookbook
248 269 assert_equal @ecookbook.id, sub.parent.id
249 270 @ecookbook.reload
250 271 assert_equal 4, @ecookbook.children.size
251 272 end
252 273
253 274 def test_move_an_orphan_project_to_a_subproject
254 275 sub = Project.find(2)
255 276 assert sub.set_parent!(@ecookbook_sub1)
256 277 end
257 278
258 279 def test_move_a_root_project_to_a_project
259 280 sub = @ecookbook
260 281 assert sub.set_parent!(Project.find(2))
261 282 end
262 283
263 284 def test_should_not_move_a_project_to_its_children
264 285 sub = @ecookbook
265 286 assert !(sub.set_parent!(Project.find(3)))
266 287 end
267 288
268 289 def test_set_parent_should_add_roots_in_alphabetical_order
269 290 ProjectCustomField.delete_all
270 291 Project.delete_all
271 292 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
272 293 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
273 294 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
274 295 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
275 296
276 297 assert_equal 4, Project.count
277 298 assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
278 299 end
279 300
280 301 def test_set_parent_should_add_children_in_alphabetical_order
281 302 ProjectCustomField.delete_all
282 303 parent = Project.create!(:name => 'Parent', :identifier => 'parent')
283 304 Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
284 305 Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
285 306 Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
286 307 Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
287 308
288 309 parent.reload
289 310 assert_equal 4, parent.children.size
290 311 assert_equal parent.children.all.sort_by(&:name), parent.children.all
291 312 end
292 313
293 314 def test_set_parent_should_update_issue_fixed_version_associations_when_a_fixed_version_is_moved_out_of_the_hierarchy
294 315 # Parent issue with a hierarchy project's fixed version
295 316 parent_issue = Issue.find(1)
296 317 parent_issue.update_attribute(:fixed_version_id, 4)
297 318 parent_issue.reload
298 319 assert_equal 4, parent_issue.fixed_version_id
299 320
300 321 # Should keep fixed versions for the issues
301 322 issue_with_local_fixed_version = Issue.find(5)
302 323 issue_with_local_fixed_version.update_attribute(:fixed_version_id, 4)
303 324 issue_with_local_fixed_version.reload
304 325 assert_equal 4, issue_with_local_fixed_version.fixed_version_id
305 326
306 327 # Local issue with hierarchy fixed_version
307 328 issue_with_hierarchy_fixed_version = Issue.find(13)
308 329 issue_with_hierarchy_fixed_version.update_attribute(:fixed_version_id, 6)
309 330 issue_with_hierarchy_fixed_version.reload
310 331 assert_equal 6, issue_with_hierarchy_fixed_version.fixed_version_id
311 332
312 333 # Move project out of the issue's hierarchy
313 334 moved_project = Project.find(3)
314 335 moved_project.set_parent!(Project.find(2))
315 336 parent_issue.reload
316 337 issue_with_local_fixed_version.reload
317 338 issue_with_hierarchy_fixed_version.reload
318 339
319 340 assert_equal 4, issue_with_local_fixed_version.fixed_version_id, "Fixed version was not keep on an issue local to the moved project"
320 341 assert_equal nil, issue_with_hierarchy_fixed_version.fixed_version_id, "Fixed version is still set after moving the Project out of the hierarchy where the version is defined in"
321 342 assert_equal nil, parent_issue.fixed_version_id, "Fixed version is still set after moving the Version out of the hierarchy for the issue."
322 343 end
323 344
324 345 def test_parent
325 346 p = Project.find(6).parent
326 347 assert p.is_a?(Project)
327 348 assert_equal 5, p.id
328 349 end
329 350
330 351 def test_ancestors
331 352 a = Project.find(6).ancestors
332 353 assert a.first.is_a?(Project)
333 354 assert_equal [1, 5], a.collect(&:id)
334 355 end
335 356
336 357 def test_root
337 358 r = Project.find(6).root
338 359 assert r.is_a?(Project)
339 360 assert_equal 1, r.id
340 361 end
341 362
342 363 def test_children
343 364 c = Project.find(1).children
344 365 assert c.first.is_a?(Project)
345 366 assert_equal [5, 3, 4], c.collect(&:id)
346 367 end
347 368
348 369 def test_descendants
349 370 d = Project.find(1).descendants
350 371 assert d.first.is_a?(Project)
351 372 assert_equal [5, 6, 3, 4], d.collect(&:id)
352 373 end
353 374
354 375 def test_allowed_parents_should_be_empty_for_non_member_user
355 376 Role.non_member.add_permission!(:add_project)
356 377 user = User.find(9)
357 378 assert user.memberships.empty?
358 379 User.current = user
359 380 assert Project.new.allowed_parents.compact.empty?
360 381 end
361 382
362 383 def test_allowed_parents_with_add_subprojects_permission
363 384 Role.find(1).remove_permission!(:add_project)
364 385 Role.find(1).add_permission!(:add_subprojects)
365 386 User.current = User.find(2)
366 387 # new project
367 388 assert !Project.new.allowed_parents.include?(nil)
368 389 assert Project.new.allowed_parents.include?(Project.find(1))
369 390 # existing root project
370 391 assert Project.find(1).allowed_parents.include?(nil)
371 392 # existing child
372 393 assert Project.find(3).allowed_parents.include?(Project.find(1))
373 394 assert !Project.find(3).allowed_parents.include?(nil)
374 395 end
375 396
376 397 def test_allowed_parents_with_add_project_permission
377 398 Role.find(1).add_permission!(:add_project)
378 399 Role.find(1).remove_permission!(:add_subprojects)
379 400 User.current = User.find(2)
380 401 # new project
381 402 assert Project.new.allowed_parents.include?(nil)
382 403 assert !Project.new.allowed_parents.include?(Project.find(1))
383 404 # existing root project
384 405 assert Project.find(1).allowed_parents.include?(nil)
385 406 # existing child
386 407 assert Project.find(3).allowed_parents.include?(Project.find(1))
387 408 assert Project.find(3).allowed_parents.include?(nil)
388 409 end
389 410
390 411 def test_allowed_parents_with_add_project_and_subprojects_permission
391 412 Role.find(1).add_permission!(:add_project)
392 413 Role.find(1).add_permission!(:add_subprojects)
393 414 User.current = User.find(2)
394 415 # new project
395 416 assert Project.new.allowed_parents.include?(nil)
396 417 assert Project.new.allowed_parents.include?(Project.find(1))
397 418 # existing root project
398 419 assert Project.find(1).allowed_parents.include?(nil)
399 420 # existing child
400 421 assert Project.find(3).allowed_parents.include?(Project.find(1))
401 422 assert Project.find(3).allowed_parents.include?(nil)
402 423 end
403 424
404 425 def test_users_by_role
405 426 users_by_role = Project.find(1).users_by_role
406 427 assert_kind_of Hash, users_by_role
407 428 role = Role.find(1)
408 429 assert_kind_of Array, users_by_role[role]
409 430 assert users_by_role[role].include?(User.find(2))
410 431 end
411 432
412 433 def test_rolled_up_trackers
413 434 parent = Project.find(1)
414 435 parent.trackers = Tracker.find([1,2])
415 436 child = parent.children.find(3)
416 437
417 438 assert_equal [1, 2], parent.tracker_ids
418 439 assert_equal [2, 3], child.trackers.collect(&:id)
419 440
420 441 assert_kind_of Tracker, parent.rolled_up_trackers.first
421 442 assert_equal Tracker.find(1), parent.rolled_up_trackers.first
422 443
423 444 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id)
424 445 assert_equal [2, 3], child.rolled_up_trackers.collect(&:id)
425 446 end
426 447
427 448 def test_rolled_up_trackers_should_ignore_archived_subprojects
428 449 parent = Project.find(1)
429 450 parent.trackers = Tracker.find([1,2])
430 451 child = parent.children.find(3)
431 452 child.trackers = Tracker.find([1,3])
432 453 parent.children.each(&:archive)
433 454
434 455 assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
435 456 end
436 457
437 458 test "#rolled_up_trackers should ignore projects with issue_tracking module disabled" do
438 459 parent = Project.generate!
439 460 parent.trackers = Tracker.find([1, 2])
440 461 child = Project.generate_with_parent!(parent)
441 462 child.trackers = Tracker.find([2, 3])
442 463
443 464 assert_equal [1, 2, 3], parent.rolled_up_trackers.collect(&:id).sort
444 465
445 466 assert child.disable_module!(:issue_tracking)
446 467 parent.reload
447 468 assert_equal [1, 2], parent.rolled_up_trackers.collect(&:id).sort
448 469 end
449 470
450 471 test "#rolled_up_versions should include the versions for the current project" do
451 472 project = Project.generate!
452 473 parent_version_1 = Version.generate!(:project => project)
453 474 parent_version_2 = Version.generate!(:project => project)
454 475 assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions
455 476 end
456 477
457 478 test "#rolled_up_versions should include versions for a subproject" do
458 479 project = Project.generate!
459 480 parent_version_1 = Version.generate!(:project => project)
460 481 parent_version_2 = Version.generate!(:project => project)
461 482 subproject = Project.generate_with_parent!(project)
462 483 subproject_version = Version.generate!(:project => subproject)
463 484
464 485 assert_same_elements [
465 486 parent_version_1,
466 487 parent_version_2,
467 488 subproject_version
468 489 ], project.rolled_up_versions
469 490 end
470 491
471 492 test "#rolled_up_versions should include versions for a sub-subproject" do
472 493 project = Project.generate!
473 494 parent_version_1 = Version.generate!(:project => project)
474 495 parent_version_2 = Version.generate!(:project => project)
475 496 subproject = Project.generate_with_parent!(project)
476 497 sub_subproject = Project.generate_with_parent!(subproject)
477 498 sub_subproject_version = Version.generate!(:project => sub_subproject)
478 499 project.reload
479 500
480 501 assert_same_elements [
481 502 parent_version_1,
482 503 parent_version_2,
483 504 sub_subproject_version
484 505 ], project.rolled_up_versions
485 506 end
486 507
487 508 test "#rolled_up_versions should only check active projects" do
488 509 project = Project.generate!
489 510 parent_version_1 = Version.generate!(:project => project)
490 511 parent_version_2 = Version.generate!(:project => project)
491 512 subproject = Project.generate_with_parent!(project)
492 513 subproject_version = Version.generate!(:project => subproject)
493 514 assert subproject.archive
494 515 project.reload
495 516
496 517 assert !subproject.active?
497 518 assert_same_elements [parent_version_1, parent_version_2], project.rolled_up_versions
498 519 end
499 520
500 521 def test_shared_versions_none_sharing
501 522 p = Project.find(5)
502 523 v = Version.create!(:name => 'none_sharing', :project => p, :sharing => 'none')
503 524 assert p.shared_versions.include?(v)
504 525 assert !p.children.first.shared_versions.include?(v)
505 526 assert !p.root.shared_versions.include?(v)
506 527 assert !p.siblings.first.shared_versions.include?(v)
507 528 assert !p.root.siblings.first.shared_versions.include?(v)
508 529 end
509 530
510 531 def test_shared_versions_descendants_sharing
511 532 p = Project.find(5)
512 533 v = Version.create!(:name => 'descendants_sharing', :project => p, :sharing => 'descendants')
513 534 assert p.shared_versions.include?(v)
514 535 assert p.children.first.shared_versions.include?(v)
515 536 assert !p.root.shared_versions.include?(v)
516 537 assert !p.siblings.first.shared_versions.include?(v)
517 538 assert !p.root.siblings.first.shared_versions.include?(v)
518 539 end
519 540
520 541 def test_shared_versions_hierarchy_sharing
521 542 p = Project.find(5)
522 543 v = Version.create!(:name => 'hierarchy_sharing', :project => p, :sharing => 'hierarchy')
523 544 assert p.shared_versions.include?(v)
524 545 assert p.children.first.shared_versions.include?(v)
525 546 assert p.root.shared_versions.include?(v)
526 547 assert !p.siblings.first.shared_versions.include?(v)
527 548 assert !p.root.siblings.first.shared_versions.include?(v)
528 549 end
529 550
530 551 def test_shared_versions_tree_sharing
531 552 p = Project.find(5)
532 553 v = Version.create!(:name => 'tree_sharing', :project => p, :sharing => 'tree')
533 554 assert p.shared_versions.include?(v)
534 555 assert p.children.first.shared_versions.include?(v)
535 556 assert p.root.shared_versions.include?(v)
536 557 assert p.siblings.first.shared_versions.include?(v)
537 558 assert !p.root.siblings.first.shared_versions.include?(v)
538 559 end
539 560
540 561 def test_shared_versions_system_sharing
541 562 p = Project.find(5)
542 563 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
543 564 assert p.shared_versions.include?(v)
544 565 assert p.children.first.shared_versions.include?(v)
545 566 assert p.root.shared_versions.include?(v)
546 567 assert p.siblings.first.shared_versions.include?(v)
547 568 assert p.root.siblings.first.shared_versions.include?(v)
548 569 end
549 570
550 571 def test_shared_versions
551 572 parent = Project.find(1)
552 573 child = parent.children.find(3)
553 574 private_child = parent.children.find(5)
554 575
555 576 assert_equal [1,2,3], parent.version_ids.sort
556 577 assert_equal [4], child.version_ids
557 578 assert_equal [6], private_child.version_ids
558 579 assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
559 580
560 581 assert_equal 6, parent.shared_versions.size
561 582 parent.shared_versions.each do |version|
562 583 assert_kind_of Version, version
563 584 end
564 585
565 586 assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
566 587 end
567 588
568 589 def test_shared_versions_should_ignore_archived_subprojects
569 590 parent = Project.find(1)
570 591 child = parent.children.find(3)
571 592 child.archive
572 593 parent.reload
573 594
574 595 assert_equal [1,2,3], parent.version_ids.sort
575 596 assert_equal [4], child.version_ids
576 597 assert !parent.shared_versions.collect(&:id).include?(4)
577 598 end
578 599
579 600 def test_shared_versions_visible_to_user
580 601 user = User.find(3)
581 602 parent = Project.find(1)
582 603 child = parent.children.find(5)
583 604
584 605 assert_equal [1,2,3], parent.version_ids.sort
585 606 assert_equal [6], child.version_ids
586 607
587 608 versions = parent.shared_versions.visible(user)
588 609
589 610 assert_equal 4, versions.size
590 611 versions.each do |version|
591 612 assert_kind_of Version, version
592 613 end
593 614
594 615 assert !versions.collect(&:id).include?(6)
595 616 end
596 617
597 618 def test_shared_versions_for_new_project_should_include_system_shared_versions
598 619 p = Project.find(5)
599 620 v = Version.create!(:name => 'system_sharing', :project => p, :sharing => 'system')
600 621
601 622 assert_include v, Project.new.shared_versions
602 623 end
603 624
604 625 def test_next_identifier
605 626 ProjectCustomField.delete_all
606 627 Project.create!(:name => 'last', :identifier => 'p2008040')
607 628 assert_equal 'p2008041', Project.next_identifier
608 629 end
609 630
610 631 def test_next_identifier_first_project
611 632 Project.delete_all
612 633 assert_nil Project.next_identifier
613 634 end
614 635
615 636 def test_enabled_module_names
616 637 with_settings :default_projects_modules => ['issue_tracking', 'repository'] do
617 638 project = Project.new
618 639
619 640 project.enabled_module_names = %w(issue_tracking news)
620 641 assert_equal %w(issue_tracking news), project.enabled_module_names.sort
621 642 end
622 643 end
623 644
624 645 test "enabled_modules should define module by names and preserve ids" do
625 646 @project = Project.find(1)
626 647 # Remove one module
627 648 modules = @project.enabled_modules.slice(0..-2)
628 649 assert modules.any?
629 650 assert_difference 'EnabledModule.count', -1 do
630 651 @project.enabled_module_names = modules.collect(&:name)
631 652 end
632 653 @project.reload
633 654 # Ids should be preserved
634 655 assert_equal @project.enabled_module_ids.sort, modules.collect(&:id).sort
635 656 end
636 657
637 658 test "enabled_modules should enable a module" do
638 659 @project = Project.find(1)
639 660 @project.enabled_module_names = []
640 661 @project.reload
641 662 assert_equal [], @project.enabled_module_names
642 663 #with string
643 664 @project.enable_module!("issue_tracking")
644 665 assert_equal ["issue_tracking"], @project.enabled_module_names
645 666 #with symbol
646 667 @project.enable_module!(:gantt)
647 668 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
648 669 #don't add a module twice
649 670 @project.enable_module!("issue_tracking")
650 671 assert_equal ["issue_tracking", "gantt"], @project.enabled_module_names
651 672 end
652 673
653 674 test "enabled_modules should disable a module" do
654 675 @project = Project.find(1)
655 676 #with string
656 677 assert @project.enabled_module_names.include?("issue_tracking")
657 678 @project.disable_module!("issue_tracking")
658 679 assert ! @project.reload.enabled_module_names.include?("issue_tracking")
659 680 #with symbol
660 681 assert @project.enabled_module_names.include?("gantt")
661 682 @project.disable_module!(:gantt)
662 683 assert ! @project.reload.enabled_module_names.include?("gantt")
663 684 #with EnabledModule object
664 685 first_module = @project.enabled_modules.first
665 686 @project.disable_module!(first_module)
666 687 assert ! @project.reload.enabled_module_names.include?(first_module.name)
667 688 end
668 689
669 690 def test_enabled_module_names_should_not_recreate_enabled_modules
670 691 project = Project.find(1)
671 692 # Remove one module
672 693 modules = project.enabled_modules.slice(0..-2)
673 694 assert modules.any?
674 695 assert_difference 'EnabledModule.count', -1 do
675 696 project.enabled_module_names = modules.collect(&:name)
676 697 end
677 698 project.reload
678 699 # Ids should be preserved
679 700 assert_equal project.enabled_module_ids.sort, modules.collect(&:id).sort
680 701 end
681 702
682 703 def test_copy_from_existing_project
683 704 source_project = Project.find(1)
684 705 copied_project = Project.copy_from(1)
685 706
686 707 assert copied_project
687 708 # Cleared attributes
688 709 assert copied_project.id.blank?
689 710 assert copied_project.name.blank?
690 711 assert copied_project.identifier.blank?
691 712
692 713 # Duplicated attributes
693 714 assert_equal source_project.description, copied_project.description
694 715 assert_equal source_project.enabled_modules, copied_project.enabled_modules
695 716 assert_equal source_project.trackers, copied_project.trackers
696 717
697 718 # Default attributes
698 719 assert_equal 1, copied_project.status
699 720 end
700 721
701 722 def test_activities_should_use_the_system_activities
702 723 project = Project.find(1)
703 724 assert_equal project.activities, TimeEntryActivity.where(:active => true).all
704 725 end
705 726
706 727
707 728 def test_activities_should_use_the_project_specific_activities
708 729 project = Project.find(1)
709 730 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project})
710 731 assert overridden_activity.save!
711 732
712 733 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
713 734 end
714 735
715 736 def test_activities_should_not_include_the_inactive_project_specific_activities
716 737 project = Project.find(1)
717 738 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
718 739 assert overridden_activity.save!
719 740
720 741 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity found"
721 742 end
722 743
723 744 def test_activities_should_not_include_project_specific_activities_from_other_projects
724 745 project = Project.find(1)
725 746 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(2)})
726 747 assert overridden_activity.save!
727 748
728 749 assert !project.activities.include?(overridden_activity), "Project specific Activity found on a different project"
729 750 end
730 751
731 752 def test_activities_should_handle_nils
732 753 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => Project.find(1), :parent => TimeEntryActivity.first})
733 754 TimeEntryActivity.delete_all
734 755
735 756 # No activities
736 757 project = Project.find(1)
737 758 assert project.activities.empty?
738 759
739 760 # No system, one overridden
740 761 assert overridden_activity.save!
741 762 project.reload
742 763 assert_equal [overridden_activity], project.activities
743 764 end
744 765
745 766 def test_activities_should_override_system_activities_with_project_activities
746 767 project = Project.find(1)
747 768 parent_activity = TimeEntryActivity.first
748 769 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => parent_activity})
749 770 assert overridden_activity.save!
750 771
751 772 assert project.activities.include?(overridden_activity), "Project specific Activity not found"
752 773 assert !project.activities.include?(parent_activity), "System Activity found when it should have been overridden"
753 774 end
754 775
755 776 def test_activities_should_include_inactive_activities_if_specified
756 777 project = Project.find(1)
757 778 overridden_activity = TimeEntryActivity.new({:name => "Project", :project => project, :parent => TimeEntryActivity.first, :active => false})
758 779 assert overridden_activity.save!
759 780
760 781 assert project.activities(true).include?(overridden_activity), "Inactive Project specific Activity not found"
761 782 end
762 783
763 784 test 'activities should not include active System activities if the project has an override that is inactive' do
764 785 project = Project.find(1)
765 786 system_activity = TimeEntryActivity.find_by_name('Design')
766 787 assert system_activity.active?
767 788 overridden_activity = TimeEntryActivity.create!(:name => "Project", :project => project, :parent => system_activity, :active => false)
768 789 assert overridden_activity.save!
769 790
770 791 assert !project.activities.include?(overridden_activity), "Inactive Project specific Activity not found"
771 792 assert !project.activities.include?(system_activity), "System activity found when the project has an inactive override"
772 793 end
773 794
774 795 def test_close_completed_versions
775 796 Version.update_all("status = 'open'")
776 797 project = Project.find(1)
777 798 assert_not_nil project.versions.detect {|v| v.completed? && v.status == 'open'}
778 799 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
779 800 project.close_completed_versions
780 801 project.reload
781 802 assert_nil project.versions.detect {|v| v.completed? && v.status != 'closed'}
782 803 assert_not_nil project.versions.detect {|v| !v.completed? && v.status == 'open'}
783 804 end
784 805
785 806 test "#start_date should be nil if there are no issues on the project" do
786 807 project = Project.generate!
787 808 assert_nil project.start_date
788 809 end
789 810
790 811 test "#start_date should be nil when issues have no start date" do
791 812 project = Project.generate!
792 813 project.trackers << Tracker.generate!
793 814 early = 7.days.ago.to_date
794 815 Issue.generate!(:project => project, :start_date => nil)
795 816
796 817 assert_nil project.start_date
797 818 end
798 819
799 820 test "#start_date should be the earliest start date of it's issues" do
800 821 project = Project.generate!
801 822 project.trackers << Tracker.generate!
802 823 early = 7.days.ago.to_date
803 824 Issue.generate!(:project => project, :start_date => Date.today)
804 825 Issue.generate!(:project => project, :start_date => early)
805 826
806 827 assert_equal early, project.start_date
807 828 end
808 829
809 830 test "#due_date should be nil if there are no issues on the project" do
810 831 project = Project.generate!
811 832 assert_nil project.due_date
812 833 end
813 834
814 835 test "#due_date should be nil if there are no issues with due dates" do
815 836 project = Project.generate!
816 837 project.trackers << Tracker.generate!
817 838 Issue.generate!(:project => project, :due_date => nil)
818 839
819 840 assert_nil project.due_date
820 841 end
821 842
822 843 test "#due_date should be the latest due date of it's issues" do
823 844 project = Project.generate!
824 845 project.trackers << Tracker.generate!
825 846 future = 7.days.from_now.to_date
826 847 Issue.generate!(:project => project, :due_date => future)
827 848 Issue.generate!(:project => project, :due_date => Date.today)
828 849
829 850 assert_equal future, project.due_date
830 851 end
831 852
832 853 test "#due_date should be the latest due date of it's versions" do
833 854 project = Project.generate!
834 855 future = 7.days.from_now.to_date
835 856 project.versions << Version.generate!(:effective_date => future)
836 857 project.versions << Version.generate!(:effective_date => Date.today)
837 858
838 859 assert_equal future, project.due_date
839 860 end
840 861
841 862 test "#due_date should pick the latest date from it's issues and versions" do
842 863 project = Project.generate!
843 864 project.trackers << Tracker.generate!
844 865 future = 7.days.from_now.to_date
845 866 far_future = 14.days.from_now.to_date
846 867 Issue.generate!(:project => project, :due_date => far_future)
847 868 project.versions << Version.generate!(:effective_date => future)
848 869
849 870 assert_equal far_future, project.due_date
850 871 end
851 872
852 873 test "#completed_percent with no versions should be 100" do
853 874 project = Project.generate!
854 875 assert_equal 100, project.completed_percent
855 876 end
856 877
857 878 test "#completed_percent with versions should return 0 if the versions have no issues" do
858 879 project = Project.generate!
859 880 Version.generate!(:project => project)
860 881 Version.generate!(:project => project)
861 882
862 883 assert_equal 0, project.completed_percent
863 884 end
864 885
865 886 test "#completed_percent with versions should return 100 if the version has only closed issues" do
866 887 project = Project.generate!
867 888 project.trackers << Tracker.generate!
868 889 v1 = Version.generate!(:project => project)
869 890 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
870 891 v2 = Version.generate!(:project => project)
871 892 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
872 893
873 894 assert_equal 100, project.completed_percent
874 895 end
875 896
876 897 test "#completed_percent with versions should return the averaged completed percent of the versions (not weighted)" do
877 898 project = Project.generate!
878 899 project.trackers << Tracker.generate!
879 900 v1 = Version.generate!(:project => project)
880 901 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
881 902 v2 = Version.generate!(:project => project)
882 903 Issue.generate!(:project => project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
883 904
884 905 assert_equal 50, project.completed_percent
885 906 end
886 907
887 908 test "#notified_users" do
888 909 project = Project.generate!
889 910 role = Role.generate!
890 911
891 912 user_with_membership_notification = User.generate!(:mail_notification => 'selected')
892 913 Member.create!(:project => project, :roles => [role], :principal => user_with_membership_notification, :mail_notification => true)
893 914
894 915 all_events_user = User.generate!(:mail_notification => 'all')
895 916 Member.create!(:project => project, :roles => [role], :principal => all_events_user)
896 917
897 918 no_events_user = User.generate!(:mail_notification => 'none')
898 919 Member.create!(:project => project, :roles => [role], :principal => no_events_user)
899 920
900 921 only_my_events_user = User.generate!(:mail_notification => 'only_my_events')
901 922 Member.create!(:project => project, :roles => [role], :principal => only_my_events_user)
902 923
903 924 only_assigned_user = User.generate!(:mail_notification => 'only_assigned')
904 925 Member.create!(:project => project, :roles => [role], :principal => only_assigned_user)
905 926
906 927 only_owned_user = User.generate!(:mail_notification => 'only_owner')
907 928 Member.create!(:project => project, :roles => [role], :principal => only_owned_user)
908 929
909 930 assert project.notified_users.include?(user_with_membership_notification), "should include members with a mail notification"
910 931 assert project.notified_users.include?(all_events_user), "should include users with the 'all' notification option"
911 932 assert !project.notified_users.include?(no_events_user), "should not include users with the 'none' notification option"
912 933 assert !project.notified_users.include?(only_my_events_user), "should not include users with the 'only_my_events' notification option"
913 934 assert !project.notified_users.include?(only_assigned_user), "should not include users with the 'only_assigned' notification option"
914 935 assert !project.notified_users.include?(only_owned_user), "should not include users with the 'only_owner' notification option"
915 936 end
916 937 end
General Comments 0
You need to be logged in to leave comments. Login now