Mercurial > hg > soundsoftware-site
diff app/models/project.rb @ 1115:433d4f72a19b redmine-2.2
Update to Redmine SVN revision 11137 on 2.2-stable branch
author | Chris Cannam |
---|---|
date | Mon, 07 Jan 2013 12:01:42 +0000 |
parents | 5f33065ddc4b |
children | bb32da3bea34 3e4c3460b6ca |
line wrap: on
line diff
--- a/app/models/project.rb Wed Jun 27 14:54:18 2012 +0100 +++ b/app/models/project.rb Mon Jan 07 12:01:42 2013 +0000 @@ -1,5 +1,5 @@ # Redmine - project management software -# Copyright (C) 2006-2011 Jean-Philippe Lang +# Copyright (C) 2006-2012 Jean-Philippe Lang # # This program is free software; you can redistribute it and/or # modify it under the terms of the GNU General Public License @@ -20,6 +20,7 @@ # Project statuses STATUS_ACTIVE = 1 + STATUS_CLOSED = 5 STATUS_ARCHIVED = 9 # Maximum length for project identifiers @@ -27,7 +28,7 @@ # Specific overidden Activities has_many :time_entry_activities - has_many :members, :include => [:user, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" + has_many :members, :include => [:principal, :roles], :conditions => "#{User.table_name}.type='User' AND #{User.table_name}.status=#{User::STATUS_ACTIVE}" has_many :memberships, :class_name => 'Member' has_many :member_principals, :class_name => 'Member', :include => :principal, @@ -37,7 +38,7 @@ has_many :enabled_modules, :dependent => :delete_all has_and_belongs_to_many :trackers, :order => "#{Tracker.table_name}.position" - has_many :issues, :dependent => :destroy, :order => "#{Issue.table_name}.created_on DESC", :include => [:status, :tracker] + has_many :issues, :dependent => :destroy, :include => [:status, :tracker] has_many :issue_changes, :through => :issues, :source => :journals has_many :versions, :dependent => :destroy, :order => "#{Version.table_name}.effective_date DESC, #{Version.table_name}.name DESC" has_many :time_entries, :dependent => :delete_all @@ -46,7 +47,8 @@ has_many :news, :dependent => :destroy, :include => :author has_many :issue_categories, :dependent => :delete_all, :order => "#{IssueCategory.table_name}.name" has_many :boards, :dependent => :destroy, :order => "position ASC" - has_one :repository, :dependent => :destroy + has_one :repository, :conditions => ["is_default = ?", true] + has_many :repositories, :dependent => :destroy has_many :changesets, :through => :repository has_one :wiki, :dependent => :destroy # Custom field for the project issues @@ -75,18 +77,39 @@ validates_length_of :homepage, :maximum => 255 validates_length_of :identifier, :in => 1..IDENTIFIER_MAX_LENGTH # donwcase letters, digits, dashes but not digits only - validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-]*$/, :if => Proc.new { |p| p.identifier_changed? } + validates_format_of :identifier, :with => /^(?!\d+$)[a-z0-9\-_]*$/, :if => Proc.new { |p| p.identifier_changed? } # reserved words validates_exclusion_of :identifier, :in => %w( new ) + after_save :update_position_under_parent, :if => Proc.new {|project| project.name_changed?} before_destroy :delete_all_members - 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] } } - named_scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} - named_scope :all_public, { :conditions => { :is_public => true } } - named_scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} + 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] } } + scope :active, { :conditions => "#{Project.table_name}.status = #{STATUS_ACTIVE}"} + scope :status, lambda {|arg| arg.blank? ? {} : {:conditions => {:status => arg.to_i}} } + scope :all_public, { :conditions => { :is_public => true } } + scope :visible, lambda {|*args| {:conditions => Project.visible_condition(args.shift || User.current, *args) }} + scope :allowed_to, lambda {|*args| + user = User.current + permission = nil + if args.first.is_a?(Symbol) + permission = args.shift + else + user = args.shift + permission = args.shift + end + { :conditions => Project.allowed_to_condition(user, permission, *args) } + } + scope :like, lambda {|arg| + if arg.blank? + {} + else + pattern = "%#{arg.to_s.strip.downcase}%" + {:conditions => ["LOWER(identifier) LIKE :p OR LOWER(name) LIKE :p", {:p => pattern}]} + end + } - def initialize(attributes = nil) + def initialize(attributes=nil, *args) super initialized = (attributes || {}).stringify_keys @@ -100,7 +123,7 @@ self.enabled_module_names = Setting.default_projects_modules end if !initialized.key?('trackers') && !initialized.key?('tracker_ids') - self.trackers = Tracker.all + self.trackers = Tracker.sorted.all end end @@ -109,7 +132,7 @@ end def identifier_frozen? - errors[:identifier].nil? && !(new_record? || identifier.blank?) + errors[:identifier].blank? && !(new_record? || identifier.blank?) end # returns latest created projects @@ -140,12 +163,11 @@ # * :with_subprojects => limit the condition to project and its subprojects # * :member => limit the condition to the user projects def self.allowed_to_condition(user, permission, options={}) - base_statement = "#{Project.table_name}.status=#{Project::STATUS_ACTIVE}" - if perm = Redmine::AccessControl.permission(permission) - unless perm.project_module.nil? - # If the permission belongs to a project module, make sure the module is enabled - base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" - end + perm = Redmine::AccessControl.permission(permission) + base_statement = (perm && perm.read? ? "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED}" : "#{Project.table_name}.status = #{Project::STATUS_ACTIVE}") + if perm && perm.project_module + # If the permission belongs to a project module, make sure the module is enabled + base_statement << " AND #{Project.table_name}.id IN (SELECT em.project_id FROM #{EnabledModule.table_name} em WHERE em.name='#{perm.project_module}')" end if options[:project] project_statement = "#{Project.table_name}.id = #{options[:project].id}" @@ -165,7 +187,7 @@ end if user.logged? user.projects_by_role.each do |role, projects| - if role.allowed_to?(permission) + if role.allowed_to?(permission) && projects.any? statement_by_role[role] = "#{Project.table_name}.id IN (#{projects.collect(&:id).join(',')})" end end @@ -251,6 +273,23 @@ end end + def self.find_by_param(*args) + self.find(*args) + end + + def reload(*args) + @shared_versions = nil + @rolled_up_versions = nil + @rolled_up_trackers = nil + @all_issue_custom_fields = nil + @all_time_entry_custom_fields = nil + @to_param = nil + @allowed_parents = nil + @allowed_permissions = nil + @actions_allowed = nil + super + end + def to_param # id is used for projects with a numeric identifier (compatibility) @to_param ||= (identifier.to_s =~ %r{^\d*$} ? id.to_s : identifier) @@ -287,6 +326,14 @@ update_attribute :status, STATUS_ACTIVE end + def close + self_and_descendants.status(STATUS_ACTIVE).update_all :status => STATUS_CLOSED + end + + def reopen + self_and_descendants.status(STATUS_CLOSED).update_all :status => STATUS_ACTIVE + end + # Returns an array of projects the project can be moved to # by the current user def allowed_parents @@ -337,22 +384,7 @@ # Nothing to do true elsif p.nil? || (p.active? && move_possible?(p)) - # Insert the project so that target's children or root projects stay alphabetically sorted - sibs = (p.nil? ? self.class.roots : p.children) - to_be_inserted_before = sibs.detect {|c| c.name.to_s.downcase > name.to_s.downcase } - if to_be_inserted_before - move_to_left_of(to_be_inserted_before) - elsif p.nil? - if sibs.empty? - # move_to_root adds the project in first (ie. left) position - move_to_root - else - move_to_right_of(sibs.last) unless self == sibs.last - end - else - # move_to_child_of adds the project in last (ie.right) position - move_to_child_of(p) - end + set_or_update_position_under(p) Issue.update_versions_from_hierarchy_change(self) true else @@ -361,12 +393,22 @@ end end + # Recalculates all lft and rgt values based on project names + # Unlike Project.rebuild!, these values are recalculated even if the tree "looks" valid + # Used in BuildProjectsTree migration + def self.rebuild_tree! + transaction do + update_all "lft = NULL, rgt = NULL" + rebuild!(false) + end + end + # Returns an array of the trackers used by the project and its active sub projects def rolled_up_trackers @rolled_up_trackers ||= Tracker.find(:all, :joins => :projects, :select => "DISTINCT #{Tracker.table_name}.*", - :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt], + :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt], :order => "#{Tracker.table_name}.position") end @@ -385,20 +427,20 @@ def rolled_up_versions @rolled_up_versions ||= Version.scoped(:include => :project, - :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status = #{STATUS_ACTIVE}", lft, rgt]) + :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ? AND #{Project.table_name}.status <> #{STATUS_ARCHIVED}", lft, rgt]) end # Returns a scope of the Versions used by the project def shared_versions if new_record? Version.scoped(:include => :project, - :conditions => "#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND #{Version.table_name}.sharing = 'system'") + :conditions => "#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND #{Version.table_name}.sharing = 'system'") else @shared_versions ||= begin r = root? ? self : root Version.scoped(:include => :project, :conditions => "#{Project.table_name}.id = #{id}" + - " OR (#{Project.table_name}.status = #{Project::STATUS_ACTIVE} AND (" + + " OR (#{Project.table_name}.status <> #{Project::STATUS_ARCHIVED} AND (" + " #{Version.table_name}.sharing = 'system'" + " OR (#{Project.table_name}.lft >= #{r.lft} AND #{Project.table_name}.rgt <= #{r.rgt} AND #{Version.table_name}.sharing = 'tree')" + " OR (#{Project.table_name}.lft < #{lft} AND #{Project.table_name}.rgt > #{rgt} AND #{Version.table_name}.sharing IN ('hierarchy', 'descendants'))" + @@ -440,7 +482,7 @@ # Returns the users that should be notified on project events def notified_users # TODO: User part should be extracted to User#notify_about? - members.select {|m| m.mail_notification? || m.user.mail_notification == 'all'}.collect {|m| m.user} + members.select {|m| m.principal.present? && (m.mail_notification? || m.principal.mail_notification == 'all')}.collect {|m| m.principal} end # Returns an array of all custom fields enabled for project issues @@ -477,6 +519,13 @@ s << ' root' if root? s << ' child' if child? s << (leaf? ? ' leaf' : ' parent') + unless active? + if archived? + s << ' archived' + else + s << ' closed' + end + end s end @@ -520,11 +569,20 @@ end end - # Return true if this project is allowed to do the specified action. + # Return true if this project allows to do the specified action. # action can be: # * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit') # * a permission Symbol (eg. :edit_project) def allows_to?(action) + if archived? + # No action allowed on archived projects + return false + end + unless active? || Redmine::AccessControl.read_action?(action) + # No write action allowed on closed projects + return false + end + # No action allowed on disabled modules if action.is_a? Hash allowed_actions.include? "#{action[:controller]}/#{action[:action]}" else @@ -715,27 +773,30 @@ end # Copies issues from +project+ - # Note: issues assigned to a closed version won't be copied due to validation rules def copy_issues(project) # Stores the source issue id as a key and the copied issues as the # value. Used to map the two togeather for issue relations. issues_map = {} + # Store status and reopen locked/closed versions + version_statuses = versions.reject(&:open?).map {|version| [version, version.status]} + version_statuses.each do |version, status| + version.update_attribute :status, 'open' + end + # Get issues sorted by root_id, lft so that parent issues # get copied before their children project.issues.find(:all, :order => 'root_id, lft').each do |issue| new_issue = Issue.new - new_issue.copy_from(issue) + new_issue.copy_from(issue, :subtasks => false, :link => false) new_issue.project = self - # Reassign fixed_versions by name, since names are unique per - # project and the versions for self are not yet saved - if issue.fixed_version - new_issue.fixed_version = self.versions.select {|v| v.name == issue.fixed_version.name}.first + # Reassign fixed_versions by name, since names are unique per project + if issue.fixed_version && issue.fixed_version.project == project + new_issue.fixed_version = self.versions.detect {|v| v.name == issue.fixed_version.name} end - # Reassign the category by name, since names are unique per - # project and the categories for self are not yet saved + # Reassign the category by name, since names are unique per project if issue.category - new_issue.category = self.issue_categories.select {|c| c.name == issue.category.name}.first + new_issue.category = self.issue_categories.detect {|c| c.name == issue.category.name} end # Parent issue if issue.parent_id @@ -752,6 +813,11 @@ end end + # Restore locked/closed version statuses + version_statuses.each do |version, status| + version.update_attribute :status, status + end + # Relations after in case issues related each other project.issues.each do |issue| new_issue = issues_map[issue.id] @@ -806,7 +872,7 @@ # Copies queries from +project+ def copy_queries(project) project.queries.each do |query| - new_query = Query.new + new_query = ::Query.new new_query.attributes = query.attributes.dup.except("id", "project_id", "sort_criteria") new_query.sort_criteria = query.sort_criteria if query.sort_criteria new_query.project = self @@ -881,4 +947,28 @@ end update_attribute :status, STATUS_ARCHIVED end + + def update_position_under_parent + set_or_update_position_under(parent) + end + + # Inserts/moves the project so that target's children or root projects stay alphabetically sorted + def set_or_update_position_under(target_parent) + sibs = (target_parent.nil? ? self.class.roots : target_parent.children) + to_be_inserted_before = sibs.sort_by {|c| c.name.to_s.downcase}.detect {|c| c.name.to_s.downcase > name.to_s.downcase } + + if to_be_inserted_before + move_to_left_of(to_be_inserted_before) + elsif target_parent.nil? + if sibs.empty? + # move_to_root adds the project in first (ie. left) position + move_to_root + else + move_to_right_of(sibs.last) unless self == sibs.last + end + else + # move_to_child_of adds the project in last (ie.right) position + move_to_child_of(target_parent) + end + end end