Mercurial > hg > soundsoftware-site
comparison app/models/project.rb @ 1338:25603efa57b5
Merge from live branch
author | Chris Cannam |
---|---|
date | Thu, 20 Jun 2013 13:14:14 +0100 |
parents | 0a574315af3e |
children | 4f746d8966dd 51364c0cd58f 467282ce64a4 |
comparison
equal
deleted
inserted
replaced
1209:1b1138f6f55e | 1338:25603efa57b5 |
---|---|
1 # Redmine - project management software | 1 # Redmine - project management software |
2 # Copyright (C) 2006-2011 Jean-Philippe Lang | 2 # Copyright (C) 2006-2012 Jean-Philippe Lang |
3 # | 3 # |
4 # This program is free software; you can redistribute it and/or | 4 # This program is free software; you can redistribute it and/or |
5 # modify it under the terms of the GNU General Public License | 5 # modify it under the terms of the GNU General Public License |
6 # as published by the Free Software Foundation; either version 2 | 6 # as published by the Free Software Foundation; either version 2 |
7 # of the License, or (at your option) any later version. | 7 # of the License, or (at your option) any later version. |
18 class Project < ActiveRecord::Base | 18 class Project < ActiveRecord::Base |
19 include Redmine::SafeAttributes | 19 include Redmine::SafeAttributes |
20 | 20 |
21 # Project statuses | 21 # Project statuses |
22 STATUS_ACTIVE = 1 | 22 STATUS_ACTIVE = 1 |
23 STATUS_CLOSED = 5 | |
23 STATUS_ARCHIVED = 9 | 24 STATUS_ARCHIVED = 9 |
24 | 25 |
25 # Maximum length for project identifiers | 26 # Maximum length for project identifiers |
26 IDENTIFIER_MAX_LENGTH = 100 | 27 IDENTIFIER_MAX_LENGTH = 100 |
27 | 28 |
28 # Specific overidden Activities | 29 # Specific overidden Activities |
29 has_many :time_entry_activities | 30 has_many :time_entry_activities |
30 has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" | 31 has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" |
31 has_many :memberships, :class_name => 'Member' | 32 has_many :memberships, :class_name => 'Member' |
32 has_many :member_principals, :class_name => 'Member', | 33 has_many :member_principals, :class_name => 'Member', |
33 :include => :principal, | 34 :include => :principal, |
34 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})" | 35 :conditions => "#{Principal.table_name}.type='Group' OR (#{Principal.table_name}.type='User' AND #{Principal.table_name}.status=#{User::STATUS_ACTIVE})" |
35 has_many :users, :through => :members | 36 has_many :users, :through => :members |
36 has_many :principals, :through => :member_principals, :source => :principal | 37 has_many :principals, :through => :member_principals, :source => :principal |
37 | 38 |
38 has_many :enabled_modules, :dependent => :delete_all | 39 has_many :enabled_modules, :dependent => :delete_all |
39 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" | 40 has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" |
40 has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] | 41 has_many :issues, :dependent => :destroy, :include => [:status, :tracker] |
41 has_many :issue_changes, :through => :issues, :source => :journals | 42 has_many :issue_changes, :through => :issues, :source => :journals |
42 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" | 43 has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" |
43 has_many :time_entries, :dependent => :delete_all | 44 has_many :time_entries, :dependent => :delete_all |
44 has_many :queries, :dependent => :delete_all | 45 has_many :queries, :dependent => :delete_all |
45 has_many :documents, :dependent => :destroy | 46 has_many :documents, :dependent => :destroy |
46 has_many :news, :dependent => :destroy, :include => :author | 47 has_many :news, :dependent => :destroy, :include => :author |
47 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" | 48 has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" |
48 has_many :boards, :dependent => :destroy, :order => "position ASC" | 49 has_many :boards, :dependent => :destroy, :order => "position ASC" |
49 has_one :repository, :dependent => :destroy | 50 has_one :repository, :conditions => ["is_default = ?", true] |
51 has_many :repositories, :dependent => :destroy | |
50 has_many :changesets, :through => :repository | 52 has_many :changesets, :through => :repository |
51 has_one :wiki, :dependent => :destroy | 53 has_one :wiki, :dependent => :destroy |
52 # Custom field for the project issues | 54 # Custom field for the project issues |
53 has_and_belongs_to_many :issue_custom_fields, | 55 has_and_belongs_to_many :issue_custom_fields, |
54 :class_name => 'IssueCustomField', | 56 :class_name => 'IssueCustomField', |
73 validates_associated :repository, :wiki | 75 validates_associated :repository, :wiki |
74 validates_length_of :name, :maximum => 255 | 76 validates_length_of :name, :maximum => 255 |
75 validates_length_of :homepage, :maximum => 255 | 77 validates_length_of :homepage, :maximum => 255 |
76 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH | 78 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH |
77 # donwcase letters, digits, dashes but not digits only | 79 # donwcase letters, digits, dashes but not digits only |
78 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? } | 80 validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? } |
79 # reserved words | 81 # reserved words |
80 validates_exclusion_of :identifier, :in => %w( new ) | 82 validates_exclusion_of :identifier, :in => %w( new ) |
81 | 83 |
84 after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} | |
82 before_destroy :delete_all_members | 85 before_destroy :delete_all_members |
83 | 86 |
84 named_scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } | 87 scope :has_module, lambda { |mod| { :conditions => ["#{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name=?)", mod.to_s] } } |
85 named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} | 88 scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} |
86 named_scope :all_public, { :conditions => { :is_public => true } } | 89 scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} } |
87 named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} | 90 scope :all_public, { :conditions => { :is_public => true } } |
88 named_scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } } | 91 scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} |
89 | 92 scope :visible_roots, lambda { { :conditions => Project.root_visible_by(User.current) } } |
90 def initialize(attributes = nil) | 93 scope :allowed_to, lambda {|*args| |
94 user = User.current | |
95 permission = nil | |
96 if args.first.is_a?(Symbol) | |
97 permission = args.shift | |
98 else | |
99 user = args.shift | |
100 permission = args.shift | |
101 end | |
102 { :conditions => Project.allowed_to_condition(user, permission, *args) } | |
103 } | |
104 scope :like, lambda {|arg| | |
105 if arg.blank? | |
106 {} | |
107 else | |
108 pattern = "%#{arg.to_s.strip.downcase}%" | |
109 {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]} | |
110 end | |
111 } | |
112 | |
113 def initialize(attributes=nil, *args) | |
91 super | 114 super |
92 | 115 |
93 initialized = (attributes || {}).stringify_keys | 116 initialized = (attributes || {}).stringify_keys |
94 if !initialized.key?('identifier') && Setting.sequential_project_identifiers? | 117 if !initialized.key?('identifier') && Setting.sequential_project_identifiers? |
95 self.identifier = Project.next_identifier | 118 self.identifier = Project.next_identifier |
99 end | 122 end |
100 if !initialized.key?('enabled_module_names') | 123 if !initialized.key?('enabled_module_names') |
101 self.enabled_module_names = Setting.default_projects_modules | 124 self.enabled_module_names = Setting.default_projects_modules |
102 end | 125 end |
103 if !initialized.key?('trackers') && !initialized.key?('tracker_ids') | 126 if !initialized.key?('trackers') && !initialized.key?('tracker_ids') |
104 self.trackers = Tracker.all | 127 self.trackers = Tracker.sorted.all |
105 end | 128 end |
106 end | 129 end |
107 | 130 |
108 def identifier=(identifier) | 131 def identifier=(identifier) |
109 super unless identifier_frozen? | 132 super unless identifier_frozen? |
110 end | 133 end |
111 | 134 |
112 def identifier_frozen? | 135 def identifier_frozen? |
113 errors[:identifier].nil? && !(new_record? || identifier.blank?) | 136 errors[:identifier].blank? && !(new_record? || identifier.blank?) |
114 end | 137 end |
115 | 138 |
116 # returns latest created projects | 139 # returns latest created projects |
117 # non public projects will be returned only if user is a member of those | 140 # non public projects will be returned only if user is a member of those |
118 def self.latest(user=nil, count=5) | 141 def self.latest(user=nil, count=5) |
143 # Valid options: | 166 # Valid options: |
144 # * :project => limit the condition to project | 167 # * :project => limit the condition to project |
145 # * :with_subprojects => limit the condition to project and its subprojects | 168 # * :with_subprojects => limit the condition to project and its subprojects |
146 # * :member => limit the condition to the user projects | 169 # * :member => limit the condition to the user projects |
147 def self.allowed_to_condition(user, permission, options={}) | 170 def self.allowed_to_condition(user, permission, options={}) |
148 base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" | 171 perm = Redmine::AccessControl.permission(permission) |
149 if perm = Redmine::AccessControl.permission(permission) | 172 base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}") |
150 unless perm.project_module.nil? | 173 if perm && perm.project_module |
151 # If the permission belongs to a project module, make sure the module is enabled | 174 # If the permission belongs to a project module, make sure the module is enabled |
152 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" | 175 base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" |
153 end | |
154 end | 176 end |
155 if options[:project] | 177 if options[:project] |
156 project_statement = "#{Project.table_name}.id = #{options[:project].id}" | 178 project_statement = "#{Project.table_name}.id = #{options[:project].id}" |
157 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] | 179 project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] |
158 base_statement = "(#{project_statement}) AND (#{base_statement})" | 180 base_statement = "(#{project_statement}) AND (#{base_statement})" |
168 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}" | 190 statement_by_role[role] = "#{Project.table_name}.is_public = #{connection.quoted_true}" |
169 end | 191 end |
170 end | 192 end |
171 if user.logged? | 193 if user.logged? |
172 user.projects_by_role.each do |role, projects| | 194 user.projects_by_role.each do |role, projects| |
173 if role.allowed_to?(permission) | 195 if role.allowed_to?(permission) && projects.any? |
174 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" | 196 statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" |
175 end | 197 end |
176 end | 198 end |
177 end | 199 end |
178 if statement_by_role.empty? | 200 if statement_by_role.empty? |
254 else | 276 else |
255 super | 277 super |
256 end | 278 end |
257 end | 279 end |
258 | 280 |
281 def self.find_by_param(*args) | |
282 self.find(*args) | |
283 end | |
284 | |
285 def reload(*args) | |
286 @shared_versions = nil | |
287 @rolled_up_versions = nil | |
288 @rolled_up_trackers = nil | |
289 @all_issue_custom_fields = nil | |
290 @all_time_entry_custom_fields = nil | |
291 @to_param = nil | |
292 @allowed_parents = nil | |
293 @allowed_permissions = nil | |
294 @actions_allowed = nil | |
295 super | |
296 end | |
297 | |
259 def to_param | 298 def to_param |
260 # id is used for projects with a numeric identifier (compatibility) | 299 # id is used for projects with a numeric identifier (compatibility) |
261 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) | 300 @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) |
262 end | 301 end |
263 | 302 |
290 def unarchive | 329 def unarchive |
291 return false if ancestors.detect {|a| !a.active?} | 330 return false if ancestors.detect {|a| !a.active?} |
292 update_attribute :status, STATUS_ACTIVE | 331 update_attribute :status, STATUS_ACTIVE |
293 end | 332 end |
294 | 333 |
334 def close | |
335 self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED | |
336 end | |
337 | |
338 def reopen | |
339 self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE | |
340 end | |
341 | |
295 # Returns an array of projects the project can be moved to | 342 # Returns an array of projects the project can be moved to |
296 # by the current user | 343 # by the current user |
297 def allowed_parents | 344 def allowed_parents |
298 return @allowed_parents if @allowed_parents | 345 return @allowed_parents if @allowed_parents |
299 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects)) | 346 @allowed_parents = Project.find(:all, :conditions => Project.allowed_to_condition(User.current, :add_subprojects)) |
340 end | 387 end |
341 if p == parent && !p.nil? | 388 if p == parent && !p.nil? |
342 # Nothing to do | 389 # Nothing to do |
343 true | 390 true |
344 elsif p.nil? || (p.active? && move_possible?(p)) | 391 elsif p.nil? || (p.active? && move_possible?(p)) |
345 # Insert the project so that target's children or root projects stay alphabetically sorted | 392 set_or_update_position_under(p) |
346 sibs = (p.nil? ? self.class.roots : p.children) | |
347 to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase } | |
348 if to_be_inserted_before | |
349 move_to_left_of(to_be_inserted_before) | |
350 elsif p.nil? | |
351 if sibs.empty? | |
352 # move_to_root adds the project in first (ie. left) position | |
353 move_to_root | |
354 else | |
355 move_to_right_of(sibs.last) unless self == sibs.last | |
356 end | |
357 else | |
358 # move_to_child_of adds the project in last (ie.right) position | |
359 move_to_child_of(p) | |
360 end | |
361 Issue.update_versions_from_hierarchy_change(self) | 393 Issue.update_versions_from_hierarchy_change(self) |
362 true | 394 true |
363 else | 395 else |
364 # Can not move to the given target | 396 # Can not move to the given target |
365 false | 397 false |
398 end | |
399 end | |
400 | |
401 # Recalculates all lft and rgt values based on project names | |
402 # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid | |
403 # Used in BuildProjectsTree migration | |
404 def self.rebuild_tree! | |
405 transaction do | |
406 update_all "lft = NULL, rgt = NULL" | |
407 rebuild!(false) | |
366 end | 408 end |
367 end | 409 end |
368 | 410 |
369 # Returns an array of the trackers used by the project and its active sub projects | 411 # Returns an array of the trackers used by the project and its active sub projects |
370 def rolled_up_trackers | 412 def rolled_up_trackers |
371 @rolled_up_trackers ||= | 413 @rolled_up_trackers ||= |
372 Tracker.find(:all, :joins => :projects, | 414 Tracker.find(:all, :joins => :projects, |
373 :select => "DISTINCT #{Tracker.table_name}.*", | 415 :select => "DISTINCT #{Tracker.table_name}.*", |
374 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], | 416 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt], |
375 :order => "#{Tracker.table_name}.position") | 417 :order => "#{Tracker.table_name}.position") |
376 end | 418 end |
377 | 419 |
378 # Closes open and locked project versions that are completed | 420 # Closes open and locked project versions that are completed |
379 def close_completed_versions | 421 def close_completed_versions |
388 | 430 |
389 # Returns a scope of the Versions on subprojects | 431 # Returns a scope of the Versions on subprojects |
390 def rolled_up_versions | 432 def rolled_up_versions |
391 @rolled_up_versions ||= | 433 @rolled_up_versions ||= |
392 Version.scoped(:include => :project, | 434 Version.scoped(:include => :project, |
393 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) | 435 :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt]) |
394 end | 436 end |
395 | 437 |
396 # Returns a scope of the Versions used by the project | 438 # Returns a scope of the Versions used by the project |
397 def shared_versions | 439 def shared_versions |
398 if new_record? | 440 if new_record? |
399 Version.scoped(:include => :project, | 441 Version.scoped(:include => :project, |
400 :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'") | 442 :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'") |
401 else | 443 else |
402 @shared_versions ||= begin | 444 @shared_versions ||= begin |
403 r = root? ? self : root | 445 r = root? ? self : root |
404 Version.scoped(:include => :project, | 446 Version.scoped(:include => :project, |
405 :conditions => "#{Project.table_name}.id = #{id}" + | 447 :conditions => "#{Project.table_name}.id = #{id}" + |
406 " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + | 448 " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" + |
407 " #{Version.table_name}.sharing = 'system'" + | 449 " #{Version.table_name}.sharing = 'system'" + |
408 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + | 450 " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + |
409 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + | 451 " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + |
410 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + | 452 " OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt} AND #{Version.table_name}.sharing = 'hierarchy')" + |
411 "))") | 453 "))") |
443 end | 485 end |
444 | 486 |
445 # Returns the users that should be notified on project events | 487 # Returns the users that should be notified on project events |
446 def notified_users | 488 def notified_users |
447 # TODO: User part should be extracted to User#notify_about? | 489 # TODO: User part should be extracted to User#notify_about? |
448 members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user} | 490 members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal} |
449 end | 491 end |
450 | 492 |
451 # Returns an array of all custom fields enabled for project issues | 493 # Returns an array of all custom fields enabled for project issues |
452 # (explictly associated custom fields and custom fields enabled for all projects) | 494 # (explictly associated custom fields and custom fields enabled for all projects) |
453 def all_issue_custom_fields | 495 def all_issue_custom_fields |
471 def to_s | 513 def to_s |
472 name | 514 name |
473 end | 515 end |
474 | 516 |
475 # Returns a short description of the projects (first lines) | 517 # Returns a short description of the projects (first lines) |
476 def short_description(length = 255) | 518 def short_description(length = 200) |
477 | 519 |
478 ## The short description is used in lists, e.g. Latest projects, | 520 ## The short description is used in lists, e.g. Latest projects, |
479 ## My projects etc. It should be no more than a line or two with | 521 ## My projects etc. It should be no more than a line or two with |
480 ## no text formatting. | 522 ## no text formatting. |
481 | 523 |
486 ## That can leave too much text for us, and also we want to omit | 528 ## That can leave too much text for us, and also we want to omit |
487 ## images and the like. Truncate instead to the first CR that | 529 ## images and the like. Truncate instead to the first CR that |
488 ## follows _any_ non-blank text, and to the next word break beyond | 530 ## follows _any_ non-blank text, and to the next word break beyond |
489 ## "length" characters if the result is still longer than that. | 531 ## "length" characters if the result is still longer than that. |
490 ## | 532 ## |
491 description.gsub(/![^\s]+!/, '').gsub(/^(\s*[^\n\r]*).*$/m, '\1').gsub(/^(.{#{length}}\b).*$/m, '\1 ...').strip if description | 533 description.gsub(/![^\s]+!/, '').gsub(/^(\s*[^\n\r]*).*$/m, '\1').gsub(/^(.{#{length}}[^\.;:,-]*).*$/m, '\1 ...').strip if description |
492 end | 534 end |
493 | 535 |
494 def css_classes | 536 def css_classes |
495 s = 'project' | 537 s = 'project' |
496 s << ' root' if root? | 538 s << ' root' if root? |
497 s << ' child' if child? | 539 s << ' child' if child? |
498 s << (leaf? ? ' leaf' : ' parent') | 540 s << (leaf? ? ' leaf' : ' parent') |
541 unless active? | |
542 if archived? | |
543 s << ' archived' | |
544 else | |
545 s << ' closed' | |
546 end | |
547 end | |
499 s | 548 s |
500 end | 549 end |
501 | 550 |
502 # The earliest start date of a project, based on it's issues and versions | 551 # The earliest start date of a project, based on it's issues and versions |
503 def start_date | 552 def start_date |
537 100 | 586 100 |
538 end | 587 end |
539 end | 588 end |
540 end | 589 end |
541 | 590 |
542 # Return true if this project is allowed to do the specified action. | 591 # Return true if this project allows to do the specified action. |
543 # action can be: | 592 # action can be: |
544 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') | 593 # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') |
545 # * a permission Symbol (eg. :edit_project) | 594 # * a permission Symbol (eg. :edit_project) |
546 def allows_to?(action) | 595 def allows_to?(action) |
596 if archived? | |
597 # No action allowed on archived projects | |
598 return false | |
599 end | |
600 unless active? || Redmine::AccessControl.read_action?(action) | |
601 # No write action allowed on closed projects | |
602 return false | |
603 end | |
604 # No action allowed on disabled modules | |
547 if action.is_a? Hash | 605 if action.is_a? Hash |
548 allowed_actions.include? "#{action[:controller]}/#{action[:action]}" | 606 allowed_actions.include? "#{action[:controller]}/#{action[:action]}" |
549 else | 607 else |
550 allowed_permissions.include? action | 608 allowed_permissions.include? action |
551 end | 609 end |
691 | 749 |
692 # Copies wiki from +project+ | 750 # Copies wiki from +project+ |
693 def copy_wiki(project) | 751 def copy_wiki(project) |
694 # Check that the source project has a wiki first | 752 # Check that the source project has a wiki first |
695 unless project.wiki.nil? | 753 unless project.wiki.nil? |
696 self.wiki ||= Wiki.new | 754 wiki = self.wiki || Wiki.new |
697 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id") | 755 wiki.attributes = project.wiki.attributes.dup.except("id", "project_id") |
698 wiki_pages_map = {} | 756 wiki_pages_map = {} |
699 project.wiki.pages.each do |page| | 757 project.wiki.pages.each do |page| |
700 # Skip pages without content | 758 # Skip pages without content |
701 next if page.content.nil? | 759 next if page.content.nil? |
703 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id")) | 761 new_wiki_page = WikiPage.new(page.attributes.dup.except("id", "wiki_id", "created_on", "parent_id")) |
704 new_wiki_page.content = new_wiki_content | 762 new_wiki_page.content = new_wiki_content |
705 wiki.pages << new_wiki_page | 763 wiki.pages << new_wiki_page |
706 wiki_pages_map[page.id] = new_wiki_page | 764 wiki_pages_map[page.id] = new_wiki_page |
707 end | 765 end |
766 | |
767 self.wiki = wiki | |
708 wiki.save | 768 wiki.save |
709 # Reproduce page hierarchy | 769 # Reproduce page hierarchy |
710 project.wiki.pages.each do |page| | 770 project.wiki.pages.each do |page| |
711 if page.parent_id && wiki_pages_map[page.id] | 771 if page.parent_id && wiki_pages_map[page.id] |
712 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id] | 772 wiki_pages_map[page.id].parent = wiki_pages_map[page.parent_id] |
733 self.issue_categories << new_issue_category | 793 self.issue_categories << new_issue_category |
734 end | 794 end |
735 end | 795 end |
736 | 796 |
737 # Copies issues from +project+ | 797 # Copies issues from +project+ |
738 # Note: issues assigned to a closed version won't be copied due to validation rules | |
739 def copy_issues(project) | 798 def copy_issues(project) |
740 # Stores the source issue id as a key and the copied issues as the | 799 # Stores the source issue id as a key and the copied issues as the |
741 # value. Used to map the two togeather for issue relations. | 800 # value. Used to map the two togeather for issue relations. |
742 issues_map = {} | 801 issues_map = {} |
743 | 802 |
803 # Store status and reopen locked/closed versions | |
804 version_statuses = versions.reject(&:open?).map {|version| [version, version.status]} | |
805 version_statuses.each do |version, status| | |
806 version.update_attribute :status, 'open' | |
807 end | |
808 | |
744 # Get issues sorted by root_id, lft so that parent issues | 809 # Get issues sorted by root_id, lft so that parent issues |
745 # get copied before their children | 810 # get copied before their children |
746 project.issues.find(:all, :order => 'root_id, lft').each do |issue| | 811 project.issues.find(:all, :order => 'root_id, lft').each do |issue| |
747 new_issue = Issue.new | 812 new_issue = Issue.new |
748 new_issue.copy_from(issue) | 813 new_issue.copy_from(issue, :subtasks => false, :link => false) |
749 new_issue.project = self | 814 new_issue.project = self |
750 # Reassign fixed_versions by name, since names are unique per | 815 # Reassign fixed_versions by name, since names are unique per project |
751 # project and the versions for self are not yet saved | 816 if issue.fixed_version && issue.fixed_version.project == project |
752 if issue.fixed_version | 817 new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name} |
753 new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first | 818 end |
754 end | 819 # Reassign the category by name, since names are unique per project |
755 # Reassign the category by name, since names are unique per | |
756 # project and the categories for self are not yet saved | |
757 if issue.category | 820 if issue.category |
758 new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first | 821 new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name} |
759 end | 822 end |
760 # Parent issue | 823 # Parent issue |
761 if issue.parent_id | 824 if issue.parent_id |
762 if copied_parent = issues_map[issue.parent_id] | 825 if copied_parent = issues_map[issue.parent_id] |
763 new_issue.parent_issue_id = copied_parent.id | 826 new_issue.parent_issue_id = copied_parent.id |
768 if new_issue.new_record? | 831 if new_issue.new_record? |
769 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info | 832 logger.info "Project#copy_issues: issue ##{issue.id} could not be copied: #{new_issue.errors.full_messages}" if logger && logger.info |
770 else | 833 else |
771 issues_map[issue.id] = new_issue unless new_issue.new_record? | 834 issues_map[issue.id] = new_issue unless new_issue.new_record? |
772 end | 835 end |
836 end | |
837 | |
838 # Restore locked/closed version statuses | |
839 version_statuses.each do |version, status| | |
840 version.update_attribute :status, status | |
773 end | 841 end |
774 | 842 |
775 # Relations after in case issues related each other | 843 # Relations after in case issues related each other |
776 project.issues.each do |issue| | 844 project.issues.each do |issue| |
777 new_issue = issues_map[issue.id] | 845 new_issue = issues_map[issue.id] |
824 end | 892 end |
825 | 893 |
826 # Copies queries from +project+ | 894 # Copies queries from +project+ |
827 def copy_queries(project) | 895 def copy_queries(project) |
828 project.queries.each do |query| | 896 project.queries.each do |query| |
829 new_query = Query.new | 897 new_query = ::Query.new |
830 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") | 898 new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") |
831 new_query.sort_criteria = query.sort_criteria if query.sort_criteria | 899 new_query.sort_criteria = query.sort_criteria if query.sort_criteria |
832 new_query.project = self | 900 new_query.project = self |
833 new_query.user_id = query.user_id | 901 new_query.user_id = query.user_id |
834 self.queries << new_query | 902 self.queries << new_query |
899 children.each do |subproject| | 967 children.each do |subproject| |
900 subproject.send :archive! | 968 subproject.send :archive! |
901 end | 969 end |
902 update_attribute :status, STATUS_ARCHIVED | 970 update_attribute :status, STATUS_ARCHIVED |
903 end | 971 end |
972 | |
973 def update_position_under_parent | |
974 set_or_update_position_under(parent) | |
975 end | |
976 | |
977 # Inserts/moves the project so that target's children or root projects stay alphabetically sorted | |
978 def set_or_update_position_under(target_parent) | |
979 sibs = (target_parent.nil? ? self.class.roots : target_parent.children) | |
980 to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } | |
981 | |
982 if to_be_inserted_before | |
983 move_to_left_of(to_be_inserted_before) | |
984 elsif target_parent.nil? | |
985 if sibs.empty? | |
986 # move_to_root adds the project in first (ie. left) position | |
987 move_to_root | |
988 else | |
989 move_to_right_of(sibs.last) unless self == sibs.last | |
990 end | |
991 else | |
992 # move_to_child_of adds the project in last (ie.right) position | |
993 move_to_child_of(target_parent) | |
994 end | |
995 end | |
904 end | 996 end |