diff --git a/app/controllers/admin_controller.rb b/app/controllers/admin_controller.rb index 45a20bf0..61d1eada 100644 --- a/app/controllers/admin_controller.rb +++ b/app/controllers/admin_controller.rb @@ -26,9 +26,6 @@ class AdminController < ApplicationController end def projects - sort_init 'name', 'asc' - sort_update %w(name is_public created_on) - @status = params[:status] ? params[:status].to_i : 1 c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) @@ -37,14 +34,8 @@ class AdminController < ApplicationController c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name] end - @project_count = Project.count(:conditions => c.conditions) - @project_pages = Paginator.new self, @project_count, - per_page_option, - params['page'] - @projects = Project.find :all, :order => sort_clause, - :conditions => c.conditions, - :limit => @project_pages.items_per_page, - :offset => @project_pages.current.offset + @projects = Project.find :all, :order => 'lft', + :conditions => c.conditions render :action => "projects", :layout => false if request.xhr? end diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 2610ca6b..64040e3b 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -43,17 +43,14 @@ class ProjectsController < ApplicationController # Lists visible projects def index - projects = Project.find :all, - :conditions => Project.visible_by(User.current), - :include => :parent respond_to do |format| format.html { - @project_tree = projects.group_by {|p| p.parent || p} - @project_tree.keys.each {|p| @project_tree[p] -= [p]} + @projects = Project.visible.find(:all, :order => 'lft') } format.atom { - render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i), - :title => "#{Setting.app_title}: #{l(:label_project_latest)}") + projects = Project.visible.find(:all, :order => 'created_on DESC', + :limit => Setting.feeds_limit.to_i) + render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}") } end end @@ -62,9 +59,6 @@ class ProjectsController < ApplicationController def add @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @trackers = Tracker.all - @root_projects = Project.find(:all, - :conditions => "parent_id IS NULL AND status = #{Project::STATUS_ACTIVE}", - :order => 'name') @project = Project.new(params[:project]) if request.get? @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? @@ -74,6 +68,7 @@ class ProjectsController < ApplicationController else @project.enabled_module_names = params[:enabled_modules] if @project.save + @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id') flash[:notice] = l(:notice_successful_create) redirect_to :controller => 'admin', :action => 'projects' end @@ -88,7 +83,8 @@ class ProjectsController < ApplicationController end @members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} - @subprojects = @project.children.find(:all, :conditions => Project.visible_by(User.current)) + @subprojects = @project.children.visible + @ancestors = @project.ancestors.visible @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC") @trackers = @project.rolled_up_trackers @@ -110,9 +106,6 @@ class ProjectsController < ApplicationController end def settings - @root_projects = Project.find(:all, - :conditions => ["parent_id IS NULL AND status = #{Project::STATUS_ACTIVE} AND id <> ?", @project.id], - :order => 'name') @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @issue_category ||= IssueCategory.new @member ||= @project.members.new @@ -126,6 +119,7 @@ class ProjectsController < ApplicationController if request.post? @project.attributes = params[:project] if @project.save + @project.set_parent!(params[:project]['parent_id']) if User.current.admin? && params[:project].has_key?('parent_id') flash[:notice] = l(:notice_successful_update) redirect_to :action => 'settings', :id => @project else diff --git a/app/controllers/reports_controller.rb b/app/controllers/reports_controller.rb index dd3ece93..bfe51577 100644 --- a/app/controllers/reports_controller.rb +++ b/app/controllers/reports_controller.rb @@ -61,7 +61,7 @@ class ReportsController < ApplicationController render :template => "reports/issue_report_details" when "subproject" @field = "project_id" - @rows = @project.active_children + @rows = @project.descendants.active @data = issues_by_subproject @report_title = l(:field_subproject) render :template => "reports/issue_report_details" @@ -72,7 +72,7 @@ class ReportsController < ApplicationController @categories = @project.issue_categories @assignees = @project.members.collect { |m| m.user } @authors = @project.members.collect { |m| m.user } - @subprojects = @project.active_children + @subprojects = @project.descendants.active issues_by_tracker issues_by_version issues_by_priority @@ -229,8 +229,8 @@ private #{Issue.table_name} i, #{IssueStatus.table_name} s where i.status_id=s.id - and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')}) - group by s.id, s.is_closed, i.project_id") if @project.active_children.any? + and i.project_id IN (#{@project.descendants.active.collect{|p| p.id}.join(',')}) + group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any? @issues_by_subproject ||= [] end end diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index e6e66f05..485d2349 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -34,7 +34,7 @@ class SearchController < ApplicationController when 'my_projects' User.current.memberships.collect(&:project) when 'subprojects' - @project ? ([ @project ] + @project.active_children) : nil + @project ? (@project.self_and_descendants.active) : nil else @project end diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb index 4c930282..ced17d66 100644 --- a/app/controllers/users_controller.rb +++ b/app/controllers/users_controller.rb @@ -83,7 +83,7 @@ class UsersController < ApplicationController end @auth_sources = AuthSource.find(:all) @roles = Role.find_all_givable - @projects = Project.find(:all, :order => 'name', :conditions => "status=#{Project::STATUS_ACTIVE}") - @user.projects + @projects = Project.active.find(:all, :order => 'lft') @membership ||= Member.new @memberships = @user.memberships end diff --git a/app/helpers/admin_helper.rb b/app/helpers/admin_helper.rb index 8f81f66b..b49a5674 100644 --- a/app/helpers/admin_helper.rb +++ b/app/helpers/admin_helper.rb @@ -20,4 +20,12 @@ module AdminHelper options_for_select([[l(:label_all), ''], [l(:status_active), 1]], selected) end + + def css_project_classes(project) + s = 'project' + s << ' root' if project.root? + s << ' child' if project.child? + s << (project.leaf? ? ' leaf' : ' parent') + s + end end diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb index 35d1670a..375d030c 100644 --- a/app/helpers/application_helper.rb +++ b/app/helpers/application_helper.rb @@ -156,6 +156,45 @@ module ApplicationHelper end s end + + # Renders the project quick-jump box + def render_project_jump_box + # Retrieve them now to avoid a COUNT query + projects = User.current.projects.all + if projects.any? + s = '' + s + end + end + + def project_tree_options_for_select(projects, options = {}) + s = '' + project_tree(projects) do |project, level| + name_prefix = (level > 0 ? (' ' * 2 * level + '» ') : '') + tag_options = {:value => project.id, :selected => ((project == options[:selected]) ? 'selected' : nil)} + tag_options.merge!(yield(project)) if block_given? + s << content_tag('option', name_prefix + h(project), tag_options) + end + s + end + + # Yields the given block for each project with its level in the tree + def project_tree(projects, &block) + ancestors = [] + projects.sort_by(&:lft).each do |project| + while (ancestors.any? && !project.is_descendant_of?(ancestors.last)) + ancestors.pop + end + yield project, ancestors.size + ancestors << project + end + end # Truncates and returns the string as a single line def truncate_single_line(string, *args) diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb index 6f5e0334..912450c1 100644 --- a/app/helpers/projects_helper.rb +++ b/app/helpers/projects_helper.rb @@ -33,4 +33,39 @@ module ProjectsHelper ] tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} end + + def parent_project_select_tag(project) + options = '' + project_tree_options_for_select(project.possible_parents, :selected => project.parent) + content_tag('select', options, :name => 'project[parent_id]') + end + + # Renders a tree of projects as a nested set of unordered lists + # The given collection may be a subset of the whole project tree + # (eg. some intermediate nodes are private and can not be seen) + def render_project_hierarchy(projects) + s = '' + if projects.any? + ancestors = [] + projects.each do |project| + if (ancestors.empty? || project.is_descendant_of?(ancestors.last)) + s << "\n" + end + end + classes = (ancestors.empty? ? 'root' : 'child') + s << "
  • " + + link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}") + s << "
    #{textilizable(project.short_description, :project => project)}
    " unless project.description.blank? + s << "
    \n" + ancestors << project + end + s << ("
  • \n" * ancestors.size) + end + s + end end diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb index cd96dbd3..32ff16f6 100644 --- a/app/helpers/search_helper.rb +++ b/app/helpers/search_helper.rb @@ -44,7 +44,7 @@ module SearchHelper def project_select_tag options = [[l(:label_project_all), 'all']] options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? - options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.active_children.empty? + options << [l(:label_and_its_subprojects, @project.name), 'subprojects'] unless @project.nil? || @project.descendants.active.empty? options << [@project.name, ''] unless @project.nil? select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 end diff --git a/app/helpers/users_helper.rb b/app/helpers/users_helper.rb index 5b113e88..b9e990d6 100644 --- a/app/helpers/users_helper.rb +++ b/app/helpers/users_helper.rb @@ -25,15 +25,10 @@ module UsersHelper end # Options for the new membership projects combo-box - def projects_options_for_select(projects) + def options_for_membership_project_select(user, projects) options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---") - projects_by_root = projects.group_by(&:root) - projects_by_root.keys.sort.each do |root| - options << content_tag('option', h(root.name), :value => root.id, :disabled => (!projects.include?(root))) - projects_by_root[root].sort.each do |project| - next if project == root - options << content_tag('option', '» ' + h(project.name), :value => project.id) - end + options << project_tree_options_for_select(projects) do |p| + {:disabled => (user.projects.include?(p))} end options end diff --git a/app/models/project.rb b/app/models/project.rb index c792b9c3..8e4bd78e 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -43,7 +43,7 @@ class Project < ActiveRecord::Base :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :association_foreign_key => 'custom_field_id' - acts_as_tree :order => "name", :counter_cache => true + acts_as_nested_set :order => 'name', :dependent => :destroy acts_as_attachable :view_permission => :view_files, :delete_permission => :manage_files @@ -66,6 +66,8 @@ class Project < ActiveRecord::Base 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 :visible, lambda { { :conditions => Project.visible_by(User.current) } } def identifier=(identifier) super unless identifier_frozen? @@ -78,7 +80,7 @@ class Project < ActiveRecord::Base def issues_with_subprojects(include_subprojects=false) conditions = nil if include_subprojects - ids = [id] + child_ids + ids = [id] + descendants.collect(&:id) conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"] end conditions ||= ["#{Project.table_name}.id = ?", id] @@ -118,7 +120,7 @@ class Project < ActiveRecord::Base end if options[:project] project_statement = "#{Project.table_name}.id = #{options[:project].id}" - project_statement << " OR #{Project.table_name}.parent_id = #{options[:project].id}" if options[:with_subprojects] + project_statement << " OR (#{Project.table_name}.lft > #{options[:project].lft} AND #{Project.table_name}.rgt < #{options[:project].rgt})" if options[:with_subprojects] base_statement = "(#{project_statement}) AND (#{base_statement})" end if user.admin? @@ -141,7 +143,7 @@ class Project < ActiveRecord::Base def project_condition(with_subprojects) cond = "#{Project.table_name}.id = #{id}" - cond = "(#{cond} OR #{Project.table_name}.parent_id = #{id})" if with_subprojects + cond = "(#{cond} OR (#{Project.table_name}.lft > #{lft} AND #{Project.table_name}.rgt < #{rgt}))" if with_subprojects cond end @@ -164,6 +166,7 @@ class Project < ActiveRecord::Base self.status == STATUS_ACTIVE end + # Archives the project and its descendants recursively def archive # Archive subprojects if any children.each do |subproject| @@ -172,13 +175,54 @@ class Project < ActiveRecord::Base update_attribute :status, STATUS_ARCHIVED end + # Unarchives the project + # All its ancestors must be active def unarchive - return false if parent && !parent.active? + return false if ancestors.detect {|a| !a.active?} update_attribute :status, STATUS_ACTIVE end - def active_children - children.select {|child| child.active?} + # Returns an array of projects the project can be moved to + def possible_parents + @possible_parents ||= (Project.active.find(:all) - self_and_descendants) + end + + # Sets the parent of the project + # Argument can be either a Project, a String, a Fixnum or nil + def set_parent!(p) + unless p.nil? || p.is_a?(Project) + if p.to_s.blank? + p = nil + else + p = Project.find_by_id(p) + return false unless p + end + end + if p == parent && !p.nil? + # 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 + true + else + # Can not move to the given target + false + end end # Returns an array of the trackers used by the project and its sub projects @@ -186,7 +230,7 @@ class Project < ActiveRecord::Base @rolled_up_trackers ||= Tracker.find(:all, :include => :projects, :select => "DISTINCT #{Tracker.table_name}.*", - :conditions => ["#{Project.table_name}.id = ? OR #{Project.table_name}.parent_id = ?", id, id], + :conditions => ["#{Project.table_name}.lft >= ? AND #{Project.table_name}.rgt <= ?", lft, rgt], :order => "#{Tracker.table_name}.position") end @@ -225,7 +269,7 @@ class Project < ActiveRecord::Base # Returns a short description of the projects (first lines) def short_description(length = 255) - description.gsub(/^(.{#{length}}[^\n]*).*$/m, '\1').strip if description + description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description end def allows_to?(action) @@ -257,8 +301,6 @@ class Project < ActiveRecord::Base protected def validate - errors.add(parent_id, " must be a root project") if parent and parent.parent - errors.add_to_base("A project with subprojects can't be a subproject") if parent and children.size > 0 errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/) end diff --git a/app/models/query.rb b/app/models/query.rb index 0016cb24..493144bf 100644 --- a/app/models/query.rb +++ b/app/models/query.rb @@ -174,8 +174,8 @@ class Query < ActiveRecord::Base unless @project.versions.empty? @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } } end - unless @project.active_children.empty? - @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } + unless @project.descendants.active.empty? + @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } } end add_custom_fields_filters(@project.all_issue_custom_fields) else @@ -257,7 +257,7 @@ class Query < ActiveRecord::Base def project_statement project_clauses = [] - if project && !@project.active_children.empty? + if project && !@project.descendants.active.empty? ids = [project.id] if has_filter?("subproject_id") case operator_for("subproject_id") @@ -268,10 +268,10 @@ class Query < ActiveRecord::Base # main project only else # all subprojects - ids += project.child_ids + ids += project.descendants.collect(&:id) end elsif Setting.display_subprojects_issues? - ids += project.child_ids + ids += project.descendants.collect(&:id) end project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') elsif project diff --git a/app/views/admin/projects.rhtml b/app/views/admin/projects.rhtml index 6c7a21fb..40177a63 100644 --- a/app/views/admin/projects.rhtml +++ b/app/views/admin/projects.rhtml @@ -17,22 +17,20 @@ - <%= sort_header_tag('name', :caption => l(:label_project)) %> + - - <%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %> - <%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %> + + <% for project in @projects %> - "> - <%= css_project_classes(project) %>"> + + + +
    <%=l(:label_project)%> <%=l(:field_description)%><%=l(:label_subproject_plural)%><%=l(:field_is_public)%><%=l(:field_created_on)%>
    <%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> - <%= textilizable project.short_description, :project => project %> - <%= project.children.size %> - <%= image_tag 'true.png' if project.is_public? %> - <%= format_date(project.created_on) %> +
    <%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %><%= textilizable project.short_description, :project => project %><%= image_tag 'true.png' if project.is_public? %><%= format_date(project.created_on) %> <%= link_to(l(:button_archive), { :controller => 'projects', :action => 'archive', :id => project }, :confirm => l(:text_are_you_sure), :method => :post, :class => 'icon icon-lock') if project.active? %> @@ -47,6 +45,4 @@
    -

    <%= pagination_links_full @project_pages, @project_count %>

    - <% html_title(l(:label_project_plural)) -%> diff --git a/app/views/layouts/_project_selector.rhtml b/app/views/layouts/_project_selector.rhtml deleted file mode 100644 index 54588404..00000000 --- a/app/views/layouts/_project_selector.rhtml +++ /dev/null @@ -1,12 +0,0 @@ -<% user_projects_by_root = User.current.projects.find(:all, :include => :parent).group_by(&:root) %> - diff --git a/app/views/layouts/base.rhtml b/app/views/layouts/base.rhtml index 0de3b0e3..6e24d9cc 100644 --- a/app/views/layouts/base.rhtml +++ b/app/views/layouts/base.rhtml @@ -34,7 +34,7 @@ <%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>: <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %> <% end %> - <%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %> + <%= render_project_jump_box %>

    <%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %>

    diff --git a/app/views/projects/_form.rhtml b/app/views/projects/_form.rhtml index f0c9fda5..24b1d9c1 100644 --- a/app/views/projects/_form.rhtml +++ b/app/views/projects/_form.rhtml @@ -4,8 +4,8 @@

    <%= f.text_field :name, :required => true %>
    <%= l(:text_caracters_maximum, 30) %>

    -<% if User.current.admin? and !@root_projects.empty? %> -

    <%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %>

    +<% if User.current.admin? && !@project.possible_parents.empty? %> +

    <%= parent_project_select_tag(@project) %>

    <% end %>

    <%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %>

    diff --git a/app/views/projects/activity.rhtml b/app/views/projects/activity.rhtml index b0e53669..67c99135 100644 --- a/app/views/projects/activity.rhtml +++ b/app/views/projects/activity.rhtml @@ -48,7 +48,7 @@

    <% @activity.event_types.each do |t| %>
    <% end %>

    -<% if @project && @project.active_children.any? %> +<% if @project && @project.descendants.active.any? %>

    <%= hidden_field_tag 'with_subprojects', 0 %> <% end %> diff --git a/app/views/projects/destroy.rhtml b/app/views/projects/destroy.rhtml index a1913c11..09d7d2a1 100644 --- a/app/views/projects/destroy.rhtml +++ b/app/views/projects/destroy.rhtml @@ -3,8 +3,8 @@

    <%=h @project_to_destroy %>
    <%=l(:text_project_destroy_confirmation)%> -<% if @project_to_destroy.children.any? %> -
    <%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.children.sort.collect{|p| p.to_s}.join(', ')))) %> +<% if @project_to_destroy.descendants.any? %> +
    <%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %> <% end %>

    diff --git a/app/views/projects/index.rhtml b/app/views/projects/index.rhtml index 4c68717f..40ea4b86 100644 --- a/app/views/projects/index.rhtml +++ b/app/views/projects/index.rhtml @@ -6,20 +6,11 @@

    <%=l(:label_project_plural)%>

    -<% @project_tree.keys.sort.each do |project| %> -

    <%= link_to h(project.name), {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %>

    -<%= textilizable(project.short_description, :project => project) %> - -<% if @project_tree[project].any? %> -

    <%= l(:label_subproject_plural) %>: - <%= @project_tree[project].sort.collect {|subproject| - link_to(h(subproject.name), {:action => 'show', :id => subproject}, :class => (User.current.member_of?(subproject) ? "icon icon-fav" : ""))}.join(', ') %>

    -<% end %> -<% end %> +<%= render_project_hierarchy(@projects)%> <% if User.current.logged? %>

    -<%= l(:label_my_projects) %> +<%= l(:label_my_projects) %>

    <% end %> diff --git a/app/views/projects/show.rhtml b/app/views/projects/show.rhtml index fa657130..6d5a1536 100644 --- a/app/views/projects/show.rhtml +++ b/app/views/projects/show.rhtml @@ -4,11 +4,13 @@ <%= textilizable @project.description %>