Commit c9906480 authored by Jean-Philippe Lang's avatar Jean-Philippe Lang

Merged nested projects branch. Removes limit on subproject nesting (#594).

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@2304 e93f8b46-1217-0410-a6f0-8f06a7374b81
parent 51b74547
...@@ -26,9 +26,6 @@ class AdminController < ApplicationController ...@@ -26,9 +26,6 @@ class AdminController < ApplicationController
end end
def projects def projects
sort_init 'name', 'asc'
sort_update %w(name is_public created_on)
@status = params[:status] ? params[:status].to_i : 1 @status = params[:status] ? params[:status].to_i : 1
c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status]) c = ARCondition.new(@status == 0 ? "status <> 0" : ["status = ?", @status])
...@@ -37,14 +34,8 @@ class AdminController < ApplicationController ...@@ -37,14 +34,8 @@ class AdminController < ApplicationController
c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name] c << ["LOWER(identifier) LIKE ? OR LOWER(name) LIKE ?", name, name]
end end
@project_count = Project.count(:conditions => c.conditions) @projects = Project.find :all, :order => 'lft',
@project_pages = Paginator.new self, @project_count, :conditions => c.conditions
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
render :action => "projects", :layout => false if request.xhr? render :action => "projects", :layout => false if request.xhr?
end end
......
...@@ -43,17 +43,14 @@ class ProjectsController < ApplicationController ...@@ -43,17 +43,14 @@ class ProjectsController < ApplicationController
# Lists visible projects # Lists visible projects
def index def index
projects = Project.find :all,
:conditions => Project.visible_by(User.current),
:include => :parent
respond_to do |format| respond_to do |format|
format.html { format.html {
@project_tree = projects.group_by {|p| p.parent || p} @projects = Project.visible.find(:all, :order => 'lft')
@project_tree.keys.each {|p| @project_tree[p] -= [p]}
} }
format.atom { format.atom {
render_feed(projects.sort_by(&:created_on).reverse.slice(0, Setting.feeds_limit.to_i), projects = Project.visible.find(:all, :order => 'created_on DESC',
:title => "#{Setting.app_title}: #{l(:label_project_latest)}") :limit => Setting.feeds_limit.to_i)
render_feed(projects, :title => "#{Setting.app_title}: #{l(:label_project_latest)}")
} }
end end
end end
...@@ -62,9 +59,6 @@ class ProjectsController < ApplicationController ...@@ -62,9 +59,6 @@ class ProjectsController < ApplicationController
def add def add
@issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position") @issue_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@trackers = Tracker.all @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]) @project = Project.new(params[:project])
if request.get? if request.get?
@project.identifier = Project.next_identifier if Setting.sequential_project_identifiers? @project.identifier = Project.next_identifier if Setting.sequential_project_identifiers?
...@@ -74,6 +68,7 @@ class ProjectsController < ApplicationController ...@@ -74,6 +68,7 @@ class ProjectsController < ApplicationController
else else
@project.enabled_module_names = params[:enabled_modules] @project.enabled_module_names = params[:enabled_modules]
if @project.save 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) flash[:notice] = l(:notice_successful_create)
redirect_to :controller => 'admin', :action => 'projects' redirect_to :controller => 'admin', :action => 'projects'
end end
...@@ -88,7 +83,8 @@ class ProjectsController < ApplicationController ...@@ -88,7 +83,8 @@ class ProjectsController < ApplicationController
end end
@members_by_role = @project.members.find(:all, :include => [:user, :role], :order => 'position').group_by {|m| m.role} @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") @news = @project.news.find(:all, :limit => 5, :include => [ :author, :project ], :order => "#{News.table_name}.created_on DESC")
@trackers = @project.rolled_up_trackers @trackers = @project.rolled_up_trackers
...@@ -110,9 +106,6 @@ class ProjectsController < ApplicationController ...@@ -110,9 +106,6 @@ class ProjectsController < ApplicationController
end end
def settings 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_custom_fields = IssueCustomField.find(:all, :order => "#{CustomField.table_name}.position")
@issue_category ||= IssueCategory.new @issue_category ||= IssueCategory.new
@member ||= @project.members.new @member ||= @project.members.new
...@@ -126,6 +119,7 @@ class ProjectsController < ApplicationController ...@@ -126,6 +119,7 @@ class ProjectsController < ApplicationController
if request.post? if request.post?
@project.attributes = params[:project] @project.attributes = params[:project]
if @project.save 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) flash[:notice] = l(:notice_successful_update)
redirect_to :action => 'settings', :id => @project redirect_to :action => 'settings', :id => @project
else else
......
...@@ -61,7 +61,7 @@ class ReportsController < ApplicationController ...@@ -61,7 +61,7 @@ class ReportsController < ApplicationController
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
when "subproject" when "subproject"
@field = "project_id" @field = "project_id"
@rows = @project.active_children @rows = @project.descendants.active
@data = issues_by_subproject @data = issues_by_subproject
@report_title = l(:field_subproject) @report_title = l(:field_subproject)
render :template => "reports/issue_report_details" render :template => "reports/issue_report_details"
...@@ -72,7 +72,7 @@ class ReportsController < ApplicationController ...@@ -72,7 +72,7 @@ class ReportsController < ApplicationController
@categories = @project.issue_categories @categories = @project.issue_categories
@assignees = @project.members.collect { |m| m.user } @assignees = @project.members.collect { |m| m.user }
@authors = @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_tracker
issues_by_version issues_by_version
issues_by_priority issues_by_priority
...@@ -229,8 +229,8 @@ private ...@@ -229,8 +229,8 @@ private
#{Issue.table_name} i, #{IssueStatus.table_name} s #{Issue.table_name} i, #{IssueStatus.table_name} s
where where
i.status_id=s.id i.status_id=s.id
and i.project_id IN (#{@project.active_children.collect{|p| p.id}.join(',')}) 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.active_children.any? group by s.id, s.is_closed, i.project_id") if @project.descendants.active.any?
@issues_by_subproject ||= [] @issues_by_subproject ||= []
end end
end end
...@@ -34,7 +34,7 @@ class SearchController < ApplicationController ...@@ -34,7 +34,7 @@ class SearchController < ApplicationController
when 'my_projects' when 'my_projects'
User.current.memberships.collect(&:project) User.current.memberships.collect(&:project)
when 'subprojects' when 'subprojects'
@project ? ([ @project ] + @project.active_children) : nil @project ? (@project.self_and_descendants.active) : nil
else else
@project @project
end end
......
...@@ -83,7 +83,7 @@ class UsersController < ApplicationController ...@@ -83,7 +83,7 @@ class UsersController < ApplicationController
end end
@auth_sources = AuthSource.find(:all) @auth_sources = AuthSource.find(:all)
@roles = Role.find_all_givable @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 @membership ||= Member.new
@memberships = @user.memberships @memberships = @user.memberships
end end
......
...@@ -20,4 +20,12 @@ module AdminHelper ...@@ -20,4 +20,12 @@ module AdminHelper
options_for_select([[l(:label_all), ''], options_for_select([[l(:label_all), ''],
[l(:status_active), 1]], selected) [l(:status_active), 1]], selected)
end 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 end
...@@ -156,6 +156,45 @@ module ApplicationHelper ...@@ -156,6 +156,45 @@ module ApplicationHelper
end end
s s
end 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 = '<select onchange="if (this.value != \'\') { window.location = this.value; }">' +
"<option selected='selected'>#{ l(:label_jump_to_a_project) }</option>" +
'<option disabled="disabled">---</option>'
s << project_tree_options_for_select(projects) do |p|
{ :value => url_for(:controller => 'projects', :action => 'show', :id => p) }
end
s << '</select>'
s
end
end
def project_tree_options_for_select(projects, options = {})
s = ''
project_tree(projects) do |project, level|
name_prefix = (level > 0 ? ('&nbsp;' * 2 * level + '&#187; ') : '')
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 # Truncates and returns the string as a single line
def truncate_single_line(string, *args) def truncate_single_line(string, *args)
......
...@@ -33,4 +33,39 @@ module ProjectsHelper ...@@ -33,4 +33,39 @@ module ProjectsHelper
] ]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)} tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
end end
def parent_project_select_tag(project)
options = '<option></option>' + 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 << "<ul class='projects #{ ancestors.empty? ? 'root' : nil}'>\n"
else
ancestors.pop
s << "</li>"
while (ancestors.any? && !project.is_descendant_of?(ancestors.last))
ancestors.pop
s << "</ul></li>\n"
end
end
classes = (ancestors.empty? ? 'root' : 'child')
s << "<li class='#{classes}'><div class='#{classes}'>" +
link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => "project #{User.current.member_of?(project) ? 'my-project' : nil}")
s << "<div class='wiki description'>#{textilizable(project.short_description, :project => project)}</div>" unless project.description.blank?
s << "</div>\n"
ancestors << project
end
s << ("</li></ul>\n" * ancestors.size)
end
s
end
end end
...@@ -44,7 +44,7 @@ module SearchHelper ...@@ -44,7 +44,7 @@ module SearchHelper
def project_select_tag def project_select_tag
options = [[l(:label_project_all), 'all']] options = [[l(:label_project_all), 'all']]
options << [l(:label_my_projects), 'my_projects'] unless User.current.memberships.empty? 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? options << [@project.name, ''] unless @project.nil?
select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1 select_tag('scope', options_for_select(options, params[:scope].to_s)) if options.size > 1
end end
......
...@@ -25,15 +25,10 @@ module UsersHelper ...@@ -25,15 +25,10 @@ module UsersHelper
end end
# Options for the new membership projects combo-box # 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)} ---") options = content_tag('option', "--- #{l(:actionview_instancetag_blank_option)} ---")
projects_by_root = projects.group_by(&:root) options << project_tree_options_for_select(projects) do |p|
projects_by_root.keys.sort.each do |root| {:disabled => (user.projects.include?(p))}
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', '&#187; ' + h(project.name), :value => project.id)
end
end end
options options
end end
......
...@@ -43,7 +43,7 @@ class Project < ActiveRecord::Base ...@@ -43,7 +43,7 @@ class Project < ActiveRecord::Base
:join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}", :join_table => "#{table_name_prefix}custom_fields_projects#{table_name_suffix}",
:association_foreign_key => 'custom_field_id' :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, acts_as_attachable :view_permission => :view_files,
:delete_permission => :manage_files :delete_permission => :manage_files
...@@ -66,6 +66,8 @@ class Project < ActiveRecord::Base ...@@ -66,6 +66,8 @@ class Project < ActiveRecord::Base
before_destroy :delete_all_members 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 :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) def identifier=(identifier)
super unless identifier_frozen? super unless identifier_frozen?
...@@ -78,7 +80,7 @@ class Project < ActiveRecord::Base ...@@ -78,7 +80,7 @@ class Project < ActiveRecord::Base
def issues_with_subprojects(include_subprojects=false) def issues_with_subprojects(include_subprojects=false)
conditions = nil conditions = nil
if include_subprojects if include_subprojects
ids = [id] + child_ids ids = [id] + descendants.collect(&:id)
conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"] conditions = ["#{Project.table_name}.id IN (#{ids.join(',')}) AND #{Project.visible_by}"]
end end
conditions ||= ["#{Project.table_name}.id = ?", id] conditions ||= ["#{Project.table_name}.id = ?", id]
...@@ -118,7 +120,7 @@ class Project < ActiveRecord::Base ...@@ -118,7 +120,7 @@ class Project < ActiveRecord::Base
end end
if options[:project] if options[:project]
project_statement = "#{Project.table_name}.id = #{options[:project].id}" 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})" base_statement = "(#{project_statement}) AND (#{base_statement})"
end end
if user.admin? if user.admin?
...@@ -141,7 +143,7 @@ class Project < ActiveRecord::Base ...@@ -141,7 +143,7 @@ class Project < ActiveRecord::Base
def project_condition(with_subprojects) def project_condition(with_subprojects)
cond = "#{Project.table_name}.id = #{id}" 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 cond
end end
...@@ -164,6 +166,7 @@ class Project < ActiveRecord::Base ...@@ -164,6 +166,7 @@ class Project < ActiveRecord::Base
self.status == STATUS_ACTIVE self.status == STATUS_ACTIVE
end end
# Archives the project and its descendants recursively
def archive def archive
# Archive subprojects if any # Archive subprojects if any
children.each do |subproject| children.each do |subproject|
...@@ -172,13 +175,54 @@ class Project < ActiveRecord::Base ...@@ -172,13 +175,54 @@ class Project < ActiveRecord::Base
update_attribute :status, STATUS_ARCHIVED update_attribute :status, STATUS_ARCHIVED
end end
# Unarchives the project
# All its ancestors must be active
def unarchive def unarchive
return false if parent && !parent.active? return false if ancestors.detect {|a| !a.active?}
update_attribute :status, STATUS_ACTIVE update_attribute :status, STATUS_ACTIVE
end end
def active_children # Returns an array of projects the project can be moved to
children.select {|child| child.active?} 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 end
# Returns an array of the trackers used by the project and its sub projects # Returns an array of the trackers used by the project and its sub projects
...@@ -186,7 +230,7 @@ class Project < ActiveRecord::Base ...@@ -186,7 +230,7 @@ class Project < ActiveRecord::Base
@rolled_up_trackers ||= @rolled_up_trackers ||=
Tracker.find(:all, :include => :projects, Tracker.find(:all, :include => :projects,
:select => "DISTINCT #{Tracker.table_name}.*", :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") :order => "#{Tracker.table_name}.position")
end end
...@@ -225,7 +269,7 @@ class Project < ActiveRecord::Base ...@@ -225,7 +269,7 @@ class Project < ActiveRecord::Base
# Returns a short description of the projects (first lines) # Returns a short description of the projects (first lines)
def short_description(length = 255) 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 end
def allows_to?(action) def allows_to?(action)
...@@ -257,8 +301,6 @@ class Project < ActiveRecord::Base ...@@ -257,8 +301,6 @@ class Project < ActiveRecord::Base
protected protected
def validate 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*$/) errors.add(:identifier, :activerecord_error_invalid) if !identifier.blank? && identifier.match(/^\d*$/)
end end
......
...@@ -174,8 +174,8 @@ class Query < ActiveRecord::Base ...@@ -174,8 +174,8 @@ class Query < ActiveRecord::Base
unless @project.versions.empty? 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] } } @available_filters["fixed_version_id"] = { :type => :list_optional, :order => 7, :values => @project.versions.sort.collect{|s| [s.name, s.id.to_s] } }
end end
unless @project.active_children.empty? unless @project.descendants.active.empty?
@available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.active_children.collect{|s| [s.name, s.id.to_s] } } @available_filters["subproject_id"] = { :type => :list_subprojects, :order => 13, :values => @project.descendants.visible.collect{|s| [s.name, s.id.to_s] } }
end end
add_custom_fields_filters(@project.all_issue_custom_fields) add_custom_fields_filters(@project.all_issue_custom_fields)
else else
...@@ -257,7 +257,7 @@ class Query < ActiveRecord::Base ...@@ -257,7 +257,7 @@ class Query < ActiveRecord::Base
def project_statement def project_statement
project_clauses = [] project_clauses = []
if project && !@project.active_children.empty? if project && !@project.descendants.active.empty?
ids = [project.id] ids = [project.id]
if has_filter?("subproject_id") if has_filter?("subproject_id")
case operator_for("subproject_id") case operator_for("subproject_id")
...@@ -268,10 +268,10 @@ class Query < ActiveRecord::Base ...@@ -268,10 +268,10 @@ class Query < ActiveRecord::Base
# main project only # main project only
else else
# all subprojects # all subprojects
ids += project.child_ids ids += project.descendants.collect(&:id)
end end
elsif Setting.display_subprojects_issues? elsif Setting.display_subprojects_issues?
ids += project.child_ids ids += project.descendants.collect(&:id)
end end
project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',') project_clauses << "#{Project.table_name}.id IN (%s)" % ids.join(',')
elsif project elsif project
......
...@@ -17,22 +17,20 @@ ...@@ -17,22 +17,20 @@
<table class="list"> <table class="list">
<thead><tr> <thead><tr>
<%= sort_header_tag('name', :caption => l(:label_project)) %> <th><%=l(:label_project)%></th>
<th><%=l(:field_description)%></th> <th><%=l(:field_description)%></th>
<th><%=l(:label_subproject_plural)%></th> <th><%=l(:field_is_public)%></th>
<%= sort_header_tag('is_public', :caption => l(:field_is_public), :default_order => 'desc') %> <th><%=l(:field_created_on)%></th>
<%= sort_header_tag('created_on', :caption => l(:field_created_on), :default_order => 'desc') %>
<th></th> <th></th>
<th></th> <th></th>
</tr></thead> </tr></thead>
<tbody> <tbody>
<% for project in @projects %> <% for project in @projects %>
<tr class="<%= cycle("odd", "even") %>"> <tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>">
<td><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %> <td class="name" style="padding-left: <%= project.level %>em;"><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %></td>
<td><%= textilizable project.short_description, :project => project %> <td><%= textilizable project.short_description, :project => project %></td>
<td align="center"><%= project.children.size %> <td align="center"><%= image_tag 'true.png' if project.is_public? %></td>
<td align="center"><%= image_tag 'true.png' if project.is_public? %> <td align="center"><%= format_date(project.created_on) %></td>
<td align="center"><%= format_date(project.created_on) %>
<td align="center" style="width:10%"> <td align="center" style="width:10%">
<small> <small>
<%= 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? %> <%= 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 @@ ...@@ -47,6 +45,4 @@
</tbody> </tbody>
</table> </table>
<p class="pagination"><%= pagination_links_full @project_pages, @project_count %></p>
<% html_title(l(:label_project_plural)) -%> <% html_title(l(:label_project_plural)) -%>
<% user_projects_by_root = User.current.projects.find(:all, :include => :parent).group_by(&:root) %>
<select onchange="if (this.value != '') { window.location = this.value; }">
<option selected="selected" value=""><%= l(:label_jump_to_a_project) %></option>
<option disabled="disabled" value="">---</option>
<% user_projects_by_root.keys.sort.each do |root| %>
<%= content_tag('option', h(root.name), :value => url_for(:controller => 'projects', :action => 'show', :id => root, :jump => current_menu_item)) %>
<% user_projects_by_root[root].sort.each do |project| %>
<% next if project == root %>
<%= content_tag('option', ('&#187; ' + h(project.name)), :value => url_for(:controller => 'projects', :action => 'show', :id => project, :jump => current_menu_item)) %>
<% end %>
<% end %>
</select>
...@@ -34,7 +34,7 @@ ...@@ -34,7 +34,7 @@
<%= link_to l(:label_search), {:controller => 'search', :action => 'index', :id => @project}, :accesskey => accesskey(:search) %>: <%= 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) %> <%= text_field_tag 'q', @question, :size => 20, :class => 'small', :accesskey => accesskey(:quick_search) %>
<% end %> <% end %>
<%= render :partial => 'layouts/project_selector' if User.current.memberships.any? %> <%= render_project_jump_box %>
</div> </div>
<h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1> <h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
......
...@@ -4,8 +4,8 @@ ...@@ -4,8 +4,8 @@
<!--[form:project]--> <!--[form:project]-->
<p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p> <p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
<% if User.current.admin? and !@root_projects.empty? %> <% if User.current.admin? && !@project.possible_parents.empty? %>
<p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p> <p><label><%= l(:field_parent) %></label><%= parent_project_select_tag(@project) %></p>
<% end %> <% end %>
<p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p> <p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
......
...@@ -48,7 +48,7 @@ ...@@ -48,7 +48,7 @@
<p><% @activity.event_types.each do |t| %> <p><% @activity.event_types.each do |t| %>
<label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br /> <label><%= check_box_tag "show_#{t}", 1, @activity.scope.include?(t) %> <%= l("label_#{t.singularize}_plural")%></label><br />
<% end %></p> <% end %></p>
<% if @project && @project.active_children.any? %> <% if @project && @project.descendants.active.any? %>
<p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p> <p><label><%= check_box_tag 'with_subprojects', 1, @with_subprojects %> <%=l(:label_subproject_plural)%></label></p>
<%= hidden_field_tag 'with_subprojects', 0 %> <%= hidden_field_tag 'with_subprojects', 0 %>
<% end %> <% end %>
......
...@@ -3,8 +3,8 @@ ...@@ -3,8 +3,8 @@
<p><strong><%=h @project_to_destroy %></strong><br /> <p><strong><%=h @project_to_destroy %></strong><br />
<%=l(:text_project_destroy_confirmation)%> <%=l(:text_project_destroy_confirmation)%>
<% if @project_to_destroy.children.any? %> <% if @project_to_destroy.descendants.any? %>
<br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.children.sort.collect{|p| p.to_s}.join(', ')))) %> <br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %>
<% end %> <% end %>
</p> </p>
<p> <p>
......
...@@ -6,20 +6,11 @@ ...@@ -6,20 +6,11 @@
<h2><%=l(:label_project_plural)%></h2> <h2><%=l(:label_project_plural)%></h2>
<% @project_tree.keys.sort.each do |project| %> <%= render_project_hierarchy(@projects)%>
<h3><%= link_to h(project.name), {:action => 'show', :id => project}, :class => (User.current.member_of?(project) ? "icon icon-fav" : "") %></h3>
<%= textilizable(project.short_description, :project => project) %>
<% if @project_tree[project].any? %>
<p><%= 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(', ') %></p>
<% end %>
<% end %>
<% if User.current.logged? %> <% if User.current.logged? %>
<p style="text-align:right;"> <p style="text-align:right;">
<span class="icon icon-fav"><%= l(:label_my_projects) %></span> <span class="my-project"><%= l(:label_my_projects) %></span>
</p> </p>
<% end %> <% end %>
......
...@@ -4,11 +4,13 @@ ...@@ -4,11 +4,13 @@
<%= textilizable @project.description %> <%= textilizable @project.description %>
<ul> <ul>
<% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %> <% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
<% if @subprojects.any? %> <% if @subprojects.any? %>
<li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li> <li><%=l(:label_subproject_plural)%>:
<% end %> <%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
<% if @project.parent %> <% end %>
<li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li> <% if @ancestors.any? %>
<li><%=l(:field_parent)%>:
<%= @ancestors.collect {|p| link_to(h(p), :action => 'show', :id => p)}.join(" &#187; ") %></li>
<% end %> <% end %>
<% @project.custom_values.each do |custom_value| %> <% @project.custom_values.each do |custom_value| %>
<% if !custom_value.value.empty? %> <% if !custom_value.value.empty? %>
......
...@@ -31,7 +31,7 @@ ...@@ -31,7 +31,7 @@
<p> <p>
<label><%=l(:label_project_new)%></label><br/> <label><%=l(:label_project_new)%></label><br/>
<% form_tag({ :action => 'edit_membership', :id => @user }) do %> <% form_tag({ :action => 'edit_membership', :id => @user }) do %>
<%= select_tag 'membership[project_id]', projects_options_for_select(@projects) %> <%= select_tag 'membership[project_id]', options_for_membership_project_select(@user, @projects) %>
<%= l(:label_role) %>: <%= l(:label_role) %>:
<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %> <%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %>
<%= submit_tag l(:button_add) %> <%= submit_tag l(:button_add) %>
......
class AddProjectsLftAndRgt < ActiveRecord::Migration
def self.up
add_column :projects, :lft, :integer
add_column :projects, :rgt, :integer
end
def self.down
remove_column :projects, :lft
remove_column :projects, :rgt
end
end
class BuildProjectsTree < ActiveRecord::Migration
def self.up
Project.rebuild!
end
def self.down
end
end
...@@ -85,6 +85,9 @@ table.list td { vertical-align: top; } ...@@ -85,6 +85,9 @@ table.list td { vertical-align: top; }
table.list td.id { width: 2%; text-align: center;} table.list td.id { width: 2%; text-align: center;}
table.list td.checkbox { width: 15px; padding: 0px;} table.list td.checkbox { width: 15px; padding: 0px;}
tr.project td.name a { padding-left: 16px; white-space:nowrap; }
tr.project.parent td.name a { background: url('../images/bullet_toggle_minus.png') no-repeat; }
tr.issue { text-align: center; white-space: nowrap; } tr.issue { text-align: center; white-space: nowrap; }
tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; } tr.issue td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
tr.issue td.subject { text-align: left; } tr.issue td.subject { text-align: left; }
...@@ -235,6 +238,15 @@ form#issue-form .attributes { margin-bottom: 8px; } ...@@ -235,6 +238,15 @@ form#issue-form .attributes { margin-bottom: 8px; }
form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; } form#issue-form .attributes p { padding-top: 1px; padding-bottom: 2px; }
form#issue-form .attributes select { min-width: 30%; } form#issue-form .attributes select { min-width: 30%; }
ul.projects { margin: 0; padding-left: 1em; }
ul.projects.root { margin: 0; padding: 0; }
ul.projects ul { border-left: 3px solid #e0e0e0; }
ul.projects li { list-style-type:none; }
ul.projects li.root { margin-bottom: 1em; }
ul.projects li.child { margin-top: 1em;}
ul.projects div.root a.project { font-family: "Trebuchet MS", Verdana, sans-serif; font-weight: bold; font-size: 16px; margin: 0 0 10px 0; }
.my-project { padding-left: 18px; background: url(../images/fav.png) no-repeat 0 50%; }
ul.properties {padding:0; font-size: 0.9em; color: #777;} ul.properties {padding:0; font-size: 0.9em; color: #777;}
ul.properties li {list-style-type:none;} ul.properties li {list-style-type:none;}
ul.properties li span {font-style:italic;} ul.properties li span {font-style:italic;}
......
...@@ -10,6 +10,8 @@ projects_001: ...@@ -10,6 +10,8 @@ projects_001:
is_public: true is_public: true
identifier: ecookbook identifier: ecookbook
parent_id: parent_id:
lft: 1
rgt: 10
projects_002: projects_002:
created_on: 2006-07-19 19:14:19 +02:00 created_on: 2006-07-19 19:14:19 +02:00
name: OnlineStore name: OnlineStore
...@@ -21,6 +23,8 @@ projects_002: ...@@ -21,6 +23,8 @@ projects_002:
is_public: false is_public: false
identifier: onlinestore identifier: onlinestore
parent_id: parent_id:
lft: 11
rgt: 12
projects_003: projects_003:
created_on: 2006-07-19 19:15:21 +02:00 created_on: 2006-07-19 19:15:21 +02:00
name: eCookbook Subproject 1 name: eCookbook Subproject 1
...@@ -32,6 +36,8 @@ projects_003: ...@@ -32,6 +36,8 @@ projects_003:
is_public: true is_public: true
identifier: subproject1 identifier: subproject1
parent_id: 1 parent_id: 1
lft: 6
rgt: 7
projects_004: projects_004:
created_on: 2006-07-19 19:15:51 +02:00 created_on: 2006-07-19 19:15:51 +02:00
name: eCookbook Subproject 2 name: eCookbook Subproject 2
...@@ -43,6 +49,8 @@ projects_004: ...@@ -43,6 +49,8 @@ projects_004:
is_public: true is_public: true
identifier: subproject2 identifier: subproject2
parent_id: 1 parent_id: 1
lft: 8
rgt: 9
projects_005: projects_005:
created_on: 2006-07-19 19:15:51 +02:00 created_on: 2006-07-19 19:15:51 +02:00
name: Private child of eCookbook name: Private child of eCookbook
...@@ -52,6 +60,21 @@ projects_005: ...@@ -52,6 +60,21 @@ projects_005:
description: This is a private subproject of a public project description: This is a private subproject of a public project
homepage: "" homepage: ""
is_public: false is_public: false
identifier: private_child identifier: private-child
parent_id: 1 parent_id: 1
lft: 2
rgt: 5
projects_006:
created_on: 2006-07-19 19:15:51 +02:00
name: Child of private child
updated_on: 2006-07-19 19:17:07 +02:00
projects_count: 0
id: 6
description: This is a public subproject of a private project
homepage: ""
is_public: true
identifier: project6
parent_id: 5
lft: 3
rgt: 4
\ No newline at end of file
...@@ -38,11 +38,18 @@ class ProjectsControllerTest < Test::Unit::TestCase ...@@ -38,11 +38,18 @@ class ProjectsControllerTest < Test::Unit::TestCase
get :index get :index
assert_response :success assert_response :success
assert_template 'index' assert_template 'index'
assert_not_nil assigns(:project_tree) assert_not_nil assigns(:projects)
# Root project as hash key
assert assigns(:project_tree).keys.include?(Project.find(1)) assert_tag :ul, :child => {:tag => 'li',
# Subproject in corresponding value :descendant => {:tag => 'a', :content => 'eCookbook'},
assert assigns(:project_tree)[Project.find(1)].include?(Project.find(3)) :child => { :tag => 'ul',
:descendant => { :tag => 'a',
:content => 'Child of private child'
}
}
}
assert_no_tag :a, :content => /Private child of eCookbook/
end end
def test_index_atom def test_index_atom
......
...@@ -45,12 +45,6 @@ class ProjectTest < Test::Unit::TestCase ...@@ -45,12 +45,6 @@ class ProjectTest < Test::Unit::TestCase
assert_equal "activerecord_error_blank", @ecookbook.errors.on(:name) assert_equal "activerecord_error_blank", @ecookbook.errors.on(:name)
end end
def test_public_projects
public_projects = Project.find(:all, :conditions => ["is_public=?", true])
assert_equal 3, public_projects.length
assert_equal true, public_projects[0].is_public?
end
def test_archive def test_archive
user = @ecookbook.members.first.user user = @ecookbook.members.first.user
@ecookbook.archive @ecookbook.archive
...@@ -60,7 +54,7 @@ class ProjectTest < Test::Unit::TestCase ...@@ -60,7 +54,7 @@ class ProjectTest < Test::Unit::TestCase
assert !user.projects.include?(@ecookbook) assert !user.projects.include?(@ecookbook)
# Subproject are also archived # Subproject are also archived
assert !@ecookbook.children.empty? assert !@ecookbook.children.empty?
assert @ecookbook.active_children.empty? assert @ecookbook.descendants.active.empty?
end end
def test_unarchive def test_unarchive
...@@ -95,25 +89,98 @@ class ProjectTest < Test::Unit::TestCase ...@@ -95,25 +89,98 @@ class ProjectTest < Test::Unit::TestCase
assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty? assert Board.find(:all, :conditions => ['project_id = ?', @ecookbook.id]).empty?
end end
def test_subproject_ok def test_move_an_orphan_project_to_a_root_project
sub = Project.find(2) sub = Project.find(2)
sub.parent = @ecookbook sub.set_parent! @ecookbook
assert sub.save
assert_equal @ecookbook.id, sub.parent.id assert_equal @ecookbook.id, sub.parent.id
@ecookbook.reload @ecookbook.reload
assert_equal 4, @ecookbook.children.size assert_equal 4, @ecookbook.children.size
end end
def test_subproject_invalid def test_move_an_orphan_project_to_a_subproject
sub = Project.find(2) sub = Project.find(2)
sub.parent = @ecookbook_sub1 assert sub.set_parent!(@ecookbook_sub1)
assert !sub.save end
def test_move_a_root_project_to_a_project
sub = @ecookbook
assert sub.set_parent!(Project.find(2))
end end
def test_subproject_invalid_2 def test_should_not_move_a_project_to_its_children
sub = @ecookbook sub = @ecookbook
sub.parent = Project.find(2) assert !(sub.set_parent!(Project.find(3)))
assert !sub.save end
def test_set_parent_should_add_roots_in_alphabetical_order
ProjectCustomField.delete_all
Project.delete_all
Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(nil)
Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(nil)
Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(nil)
Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(nil)
assert_equal 4, Project.count
assert_equal Project.all.sort_by(&:name), Project.all.sort_by(&:lft)
end
def test_set_parent_should_add_children_in_alphabetical_order
ProjectCustomField.delete_all
parent = Project.create!(:name => 'Parent', :identifier => 'parent')
Project.create!(:name => 'Project C', :identifier => 'project-c').set_parent!(parent)
Project.create!(:name => 'Project B', :identifier => 'project-b').set_parent!(parent)
Project.create!(:name => 'Project D', :identifier => 'project-d').set_parent!(parent)
Project.create!(:name => 'Project A', :identifier => 'project-a').set_parent!(parent)
parent.reload
assert_equal 4, parent.children.size
assert_equal parent.children.sort_by(&:name), parent.children
end
def test_rebuild_should_sort_children_alphabetically
ProjectCustomField.delete_all
parent = Project.create!(:name => 'Parent', :identifier => 'parent')
Project.create!(:name => 'Project C', :identifier => 'project-c').move_to_child_of(parent)
Project.create!(:name => 'Project B', :identifier => 'project-b').move_to_child_of(parent)
Project.create!(:name => 'Project D', :identifier => 'project-d').move_to_child_of(parent)
Project.create!(:name => 'Project A', :identifier => 'project-a').move_to_child_of(parent)
Project.update_all("lft = NULL, rgt = NULL")
Project.rebuild!
parent.reload
assert_equal 4, parent.children.size
assert_equal parent.children.sort_by(&:name), parent.children
end
def test_parent
p = Project.find(6).parent
assert p.is_a?(Project)
assert_equal 5, p.id
end
def test_ancestors
a = Project.find(6).ancestors
assert a.first.is_a?(Project)
assert_equal [1, 5], a.collect(&:id)
end
def test_root
r = Project.find(6).root
assert r.is_a?(Project)
assert_equal 1, r.id
end
def test_children
c = Project.find(1).children
assert c.first.is_a?(Project)
assert_equal [5, 3, 4], c.collect(&:id)
end
def test_descendants
d = Project.find(1).descendants
assert d.first.is_a?(Project)
assert_equal [5, 6, 3, 4], d.collect(&:id)
end end
def test_rolled_up_trackers def test_rolled_up_trackers
......
Copyright (c) 2007 [name of plugin creator]
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
= AwesomeNestedSet
Awesome Nested Set is an implementation of the nested set pattern for ActiveRecord models. It is replacement for acts_as_nested_set and BetterNestedSet, but awesomer.
== What makes this so awesome?
This is a new implementation of nested set based off of BetterNestedSet that fixes some bugs, removes tons of duplication, adds a few useful methods, and adds STI support.
== Installation
If you are on Rails 2.1 or later:
script/plugin install git://github.com/collectiveidea/awesome_nested_set.git
== Usage
To make use of awesome_nested_set, your model needs to have 3 fields: lft, rgt, and parent_id:
class CreateCategories < ActiveRecord::Migration
def self.up
create_table :categories do |t|
t.string :name
t.integer :parent_id
t.integer :lft
t.integer :rgt
end
end
def self.down
drop_table :categories
end
end
Enable the nested set functionality by declaring acts_as_nested_set on your model
class Category < ActiveRecord::Base
acts_as_nested_set
end
Run `rake rdoc` to generate the API docs and see CollectiveIdea::Acts::NestedSet::SingletonMethods for more info.
== View Helper
The view helper is called #nested_set_options.
Example usage:
<%= f.select :parent_id, nested_set_options(Category, @category) {|i| "#{'-' * i.level} #{i.name}" } %>
<%= select_tag 'parent_id', options_for_select(nested_set_options(Category) {|i| "#{'-' * i.level} #{i.name}" } ) %>
See CollectiveIdea::Acts::NestedSet::Helper for more information about the helpers.
== References
You can learn more about nested sets at:
http://www.dbmsmag.com/9603d06.html
http://threebit.net/tutorials/nestedset/tutorial1.html
http://api.rubyonrails.com/classes/ActiveRecord/Acts/NestedSet/ClassMethods.html
http://opensource.symetrie.com/trac/better_nested_set/
Copyright (c) 2008 Collective Idea, released under the MIT license
\ No newline at end of file
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/gempackagetask'
require 'rcov/rcovtask'
require "load_multi_rails_rake_tasks"
spec = eval(File.read("#{File.dirname(__FILE__)}/awesome_nested_set.gemspec"))
PKG_NAME = spec.name
PKG_VERSION = spec.version
Rake::GemPackageTask.new(spec) do |pkg|
pkg.need_zip = true
pkg.need_tar = true
end
desc 'Default: run unit tests.'
task :default => :test
desc 'Test the awesome_nested_set plugin.'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
desc 'Generate documentation for the awesome_nested_set plugin.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'AwesomeNestedSet'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README.rdoc')
rdoc.rdoc_files.include('lib/**/*.rb')
end
namespace :test do
desc "just rcov minus html output"
Rcov::RcovTask.new(:coverage) do |t|
# t.libs << 'test'
t.test_files = FileList['test/**/*_test.rb']
t.output_dir = 'coverage'
t.verbose = true
t.rcov_opts = %w(--exclude test,/usr/lib/ruby,/Library/Ruby,lib/awesome_nested_set/named_scope.rb --sort coverage)
end
end
\ No newline at end of file
Gem::Specification.new do |s|
s.name = "awesome_nested_set"
s.version = "1.1.1"
s.summary = "An awesome replacement for acts_as_nested_set and better_nested_set."
s.description = s.summary
s.files = %w(init.rb MIT-LICENSE Rakefile README.rdoc lib/awesome_nested_set.rb lib/awesome_nested_set/compatability.rb lib/awesome_nested_set/helper.rb lib/awesome_nested_set/named_scope.rb rails/init.rb test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml)
s.add_dependency "activerecord", ['>= 1.1']
s.has_rdoc = true
s.extra_rdoc_files = [ "README.rdoc"]
s.rdoc_options = ["--main", "README.rdoc", "--inline-source", "--line-numbers"]
s.test_files = %w(test/awesome_nested_set_test.rb test/test_helper.rb test/awesome_nested_set/helper_test.rb test/db/database.yml test/db/schema.rb test/fixtures/categories.yml test/fixtures/category.rb test/fixtures/departments.yml test/fixtures/notes.yml)
s.require_path = 'lib'
s.author = "Collective Idea"
s.email = "info@collectiveidea.com"
s.homepage = "http://collectiveidea.com"
end
require File.dirname(__FILE__) + "/rails/init"
This diff is collapsed.
# Rails <2.x doesn't define #except
class Hash #:nodoc:
# Returns a new hash without the given keys.
def except(*keys)
clone.except!(*keys)
end unless method_defined?(:except)
# Replaces the hash without the given keys.
def except!(*keys)
keys.map! { |key| convert_key(key) } if respond_to?(:convert_key)
keys.each { |key| delete(key) }
self
end unless method_defined?(:except!)
end
# NamedScope is new to Rails 2.1
unless defined? ActiveRecord::NamedScope
require 'awesome_nested_set/named_scope'
ActiveRecord::Base.class_eval do
include CollectiveIdea::NamedScope
end
end
# Rails 1.2.x doesn't define #quoted_table_name
class ActiveRecord::Base #:nodoc:
def self.quoted_table_name
self.connection.quote_column_name(self.table_name)
end unless methods.include?('quoted_table_name')
end
\ No newline at end of file
module CollectiveIdea #:nodoc:
module Acts #:nodoc:
module NestedSet #:nodoc:
# This module provides some helpers for the model classes using acts_as_nested_set.
# It is included by default in all views.
#
module Helper
# Returns options for select.
# You can exclude some items from the tree.
# You can pass a block receiving an item and returning the string displayed in the select.
#
# == Params
# * +class_or_item+ - Class name or top level times
# * +mover+ - The item that is being move, used to exlude impossible moves
# * +&block+ - a block that will be used to display: { |item| ... item.name }
#
# == Usage
#
# <%= f.select :parent_id, nested_set_options(Category, @category) {|i|
# "#{'–' * i.level} #{i.name}"
# }) %>
#
def nested_set_options(class_or_item, mover = nil)
class_or_item = class_or_item.roots if class_or_item.is_a?(Class)
items = Array(class_or_item)
result = []
items.each do |root|
result += root.self_and_descendants.map do |i|
if mover.nil? || mover.new_record? || mover.move_possible?(i)
[yield(i), i.id]
end
end.compact
end
result
end
end
end
end
end
\ No newline at end of file
# Taken from Rails 2.1
module CollectiveIdea #:nodoc:
module NamedScope #:nodoc:
# All subclasses of ActiveRecord::Base have two named_scopes:
# * <tt>all</tt>, which is similar to a <tt>find(:all)</tt> query, and
# * <tt>scoped</tt>, which allows for the creation of anonymous scopes, on the fly:
#
# Shirt.scoped(:conditions => {:color => 'red'}).scoped(:include => :washing_instructions)
#
# These anonymous scopes tend to be useful when procedurally generating complex queries, where passing
# intermediate values (scopes) around as first-class objects is convenient.
def self.included(base)
base.class_eval do
extend ClassMethods
named_scope :scoped, lambda { |scope| scope }
end
end
module ClassMethods #:nodoc:
def scopes
read_inheritable_attribute(:scopes) || write_inheritable_attribute(:scopes, {})
end
# Adds a class method for retrieving and querying objects. A scope represents a narrowing of a database query,
# such as <tt>:conditions => {:color => :red}, :select => 'shirts.*', :include => :washing_instructions</tt>.
#
# class Shirt < ActiveRecord::Base
# named_scope :red, :conditions => {:color => 'red'}
# named_scope :dry_clean_only, :joins => :washing_instructions, :conditions => ['washing_instructions.dry_clean_only = ?', true]
# end
#
# The above calls to <tt>named_scope</tt> define class methods <tt>Shirt.red</tt> and <tt>Shirt.dry_clean_only</tt>. <tt>Shirt.red</tt>,
# in effect, represents the query <tt>Shirt.find(:all, :conditions => {:color => 'red'})</tt>.
#
# Unlike Shirt.find(...), however, the object returned by <tt>Shirt.red</tt> is not an Array; it resembles the association object
# constructed by a <tt>has_many</tt> declaration. For instance, you can invoke <tt>Shirt.red.find(:first)</tt>, <tt>Shirt.red.count</tt>,
# <tt>Shirt.red.find(:all, :conditions => {:size => 'small'})</tt>. Also, just
# as with the association objects, name scopes acts like an Array, implementing Enumerable; <tt>Shirt.red.each(&block)</tt>,
# <tt>Shirt.red.first</tt>, and <tt>Shirt.red.inject(memo, &block)</tt> all behave as if Shirt.red really were an Array.
#
# These named scopes are composable. For instance, <tt>Shirt.red.dry_clean_only</tt> will produce all shirts that are both red and dry clean only.
# Nested finds and calculations also work with these compositions: <tt>Shirt.red.dry_clean_only.count</tt> returns the number of garments
# for which these criteria obtain. Similarly with <tt>Shirt.red.dry_clean_only.average(:thread_count)</tt>.
#
# All scopes are available as class methods on the ActiveRecord descendent upon which the scopes were defined. But they are also available to
# <tt>has_many</tt> associations. If,
#
# class Person < ActiveRecord::Base
# has_many :shirts
# end
#
# then <tt>elton.shirts.red.dry_clean_only</tt> will return all of Elton's red, dry clean
# only shirts.
#
# Named scopes can also be procedural.
#
# class Shirt < ActiveRecord::Base
# named_scope :colored, lambda { |color|
# { :conditions => { :color => color } }
# }
# end
#
# In this example, <tt>Shirt.colored('puce')</tt> finds all puce shirts.
#
# Named scopes can also have extensions, just as with <tt>has_many</tt> declarations:
#
# class Shirt < ActiveRecord::Base
# named_scope :red, :conditions => {:color => 'red'} do
# def dom_id
# 'red_shirts'
# end
# end
# end
#
#
# For testing complex named scopes, you can examine the scoping options using the
# <tt>proxy_options</tt> method on the proxy itself.
#
# class Shirt < ActiveRecord::Base
# named_scope :colored, lambda { |color|
# { :conditions => { :color => color } }
# }
# end
#
# expected_options = { :conditions => { :colored => 'red' } }
# assert_equal expected_options, Shirt.colored('red').proxy_options
def named_scope(name, options = {}, &block)
scopes[name] = lambda do |parent_scope, *args|
Scope.new(parent_scope, case options
when Hash
options
when Proc
options.call(*args)
end, &block)
end
(class << self; self end).instance_eval do
define_method name do |*args|
scopes[name].call(self, *args)
end
end
end
end
class Scope #:nodoc:
attr_reader :proxy_scope, :proxy_options
[].methods.each { |m| delegate m, :to => :proxy_found unless m =~ /(^__|^nil\?|^send|class|extend|find|count|sum|average|maximum|minimum|paginate)/ }
delegate :scopes, :with_scope, :to => :proxy_scope
def initialize(proxy_scope, options, &block)
[options[:extend]].flatten.each { |extension| extend extension } if options[:extend]
extend Module.new(&block) if block_given?
@proxy_scope, @proxy_options = proxy_scope, options.except(:extend)
end
def reload
load_found; self
end
protected
def proxy_found
@found || load_found
end
private
def method_missing(method, *args, &block)
if scopes.include?(method)
scopes[method].call(self, *args)
else
with_scope :find => proxy_options do
proxy_scope.send(method, *args, &block)
end
end
end
def load_found
@found = find(:all)
end
end
end
end
\ No newline at end of file
require 'awesome_nested_set/compatability'
require 'awesome_nested_set'
ActiveRecord::Base.class_eval do
include CollectiveIdea::Acts::NestedSet
end
if defined?(ActionView)
require 'awesome_nested_set/helper'
ActionView::Base.class_eval do
include CollectiveIdea::Acts::NestedSet::Helper
end
end
\ No newline at end of file
require File.dirname(__FILE__) + '/../test_helper'
module CollectiveIdea
module Acts #:nodoc:
module NestedSet #:nodoc:
class AwesomeNestedSetTest < Test::Unit::TestCase
include Helper
fixtures :categories
def test_nested_set_options
expected = [
[" Top Level", 1],
["- Child 1", 2],
['- Child 2', 3],
['-- Child 2.1', 4],
['- Child 3', 5],
[" Top Level 2", 6]
]
actual = nested_set_options(Category) do |c|
"#{'-' * c.level} #{c.name}"
end
assert_equal expected, actual
end
def test_nested_set_options_with_mover
expected = [
[" Top Level", 1],
["- Child 1", 2],
['- Child 3', 5],
[" Top Level 2", 6]
]
actual = nested_set_options(Category, categories(:child_2)) do |c|
"#{'-' * c.level} #{c.name}"
end
assert_equal expected, actual
end
end
end
end
end
\ No newline at end of file
sqlite3:
adapter: sqlite3
dbfile: awesome_nested_set.sqlite3.db
sqlite3mem:
:adapter: sqlite3
:dbfile: ":memory:"
postgresql:
:adapter: postgresql
:username: postgres
:password: postgres
:database: awesome_nested_set_plugin_test
:min_messages: ERROR
mysql:
:adapter: mysql
:host: localhost
:username: root
:password:
:database: awesome_nested_set_plugin_test
\ No newline at end of file
ActiveRecord::Schema.define(:version => 0) do
create_table :categories, :force => true do |t|
t.column :name, :string
t.column :parent_id, :integer
t.column :lft, :integer
t.column :rgt, :integer
t.column :organization_id, :integer
end
create_table :departments, :force => true do |t|
t.column :name, :string
end
create_table :notes, :force => true do |t|
t.column :body, :text
t.column :parent_id, :integer
t.column :lft, :integer
t.column :rgt, :integer
t.column :notable_id, :integer
t.column :notable_type, :string
end
end
top_level:
id: 1
name: Top Level
lft: 1
rgt: 10
child_1:
id: 2
name: Child 1
parent_id: 1
lft: 2
rgt: 3
child_2:
id: 3
name: Child 2
parent_id: 1
lft: 4
rgt: 7
child_2_1:
id: 4
name: Child 2.1
parent_id: 3
lft: 5
rgt: 6
child_3:
id: 5
name: Child 3
parent_id: 1
lft: 8
rgt: 9
top_level_2:
id: 6
name: Top Level 2
lft: 11
rgt: 12
class Category < ActiveRecord::Base
acts_as_nested_set
def to_s
name
end
def recurse &block
block.call self, lambda{
self.children.each do |child|
child.recurse &block
end
}
end
end
\ No newline at end of file
top:
id: 1
name: Top
\ No newline at end of file
scope1:
id: 1
body: Top Level
lft: 1
rgt: 10
notable_id: 1
notable_type: Category
child_1:
id: 2
body: Child 1
parent_id: 1
lft: 2
rgt: 3
notable_id: 1
notable_type: Category
child_2:
id: 3
body: Child 2
parent_id: 1
lft: 4
rgt: 7
notable_id: 1
notable_type: Category
child_3:
id: 4
body: Child 3
parent_id: 1
lft: 8
rgt: 9
notable_id: 1
notable_type: Category
scope2:
id: 5
body: Top Level 2
lft: 1
rgt: 2
notable_id: 1
notable_type: Departments
$:.unshift(File.dirname(__FILE__) + '/../lib')
plugin_test_dir = File.dirname(__FILE__)
require 'rubygems'
require 'test/unit'
require 'multi_rails_init'
# gem 'activerecord', '>= 2.0'
require 'active_record'
require 'action_controller'
require 'action_view'
require 'active_record/fixtures'
require plugin_test_dir + '/../init.rb'
ActiveRecord::Base.logger = Logger.new(plugin_test_dir + "/debug.log")
ActiveRecord::Base.configurations = YAML::load(IO.read(plugin_test_dir + "/db/database.yml"))
ActiveRecord::Base.establish_connection(ENV["DB"] || "sqlite3mem")
ActiveRecord::Migration.verbose = false
load(File.join(plugin_test_dir, "db", "schema.rb"))
Dir["#{plugin_test_dir}/fixtures/*.rb"].each {|file| require file }
class Test::Unit::TestCase #:nodoc:
self.fixture_path = File.dirname(__FILE__) + "/fixtures/"
self.use_transactional_fixtures = true
self.use_instantiated_fixtures = false
fixtures :categories, :notes, :departments
end
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment