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
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
......
......@@ -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
......
......@@ -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
......@@ -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
......
......@@ -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
......
......@@ -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
......@@ -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 = '<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
def truncate_single_line(string, *args)
......
......@@ -33,4 +33,39 @@ module ProjectsHelper
]
tabs.select {|tab| User.current.allowed_to?(tab[:action], @project)}
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
......@@ -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
......
......@@ -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', '&#187; ' + h(project.name), :value => project.id)
end
options << project_tree_options_for_select(projects) do |p|
{:disabled => (user.projects.include?(p))}
end
options
end
......
......@@ -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
......
......@@ -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
......
......@@ -17,22 +17,20 @@
<table class="list">
<thead><tr>
<%= sort_header_tag('name', :caption => l(:label_project)) %>
<th><%=l(:label_project)%></th>
<th><%=l(:field_description)%></th>
<th><%=l(:label_subproject_plural)%></th>
<%= 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') %>
<th><%=l(:field_is_public)%></th>
<th><%=l(:field_created_on)%></th>
<th></th>
<th></th>
</tr></thead>
<tbody>
<% for project in @projects %>
<tr class="<%= cycle("odd", "even") %>">
<td><%= project.active? ? link_to(h(project.name), :controller => 'projects', :action => 'settings', :id => project) : h(project.name) %>
<td><%= textilizable project.short_description, :project => project %>
<td align="center"><%= project.children.size %>
<td align="center"><%= image_tag 'true.png' if project.is_public? %>
<td align="center"><%= format_date(project.created_on) %>
<tr class="<%= cycle("odd", "even") %> <%= css_project_classes(project) %>">
<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>
<td align="center"><%= image_tag 'true.png' if project.is_public? %></td>
<td align="center"><%= format_date(project.created_on) %></td>
<td align="center" style="width:10%">
<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? %>
......@@ -47,6 +45,4 @@
</tbody>
</table>
<p class="pagination"><%= pagination_links_full @project_pages, @project_count %></p>
<% 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 @@
<%= 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 %>
</div>
<h1><%= h(@project && !@project.new_record? ? @project.name : Setting.app_title) %></h1>
......
......@@ -4,8 +4,8 @@
<!--[form:project]-->
<p><%= f.text_field :name, :required => true %><br /><em><%= l(:text_caracters_maximum, 30) %></em></p>
<% if User.current.admin? and !@root_projects.empty? %>
<p><%= f.select :parent_id, (@root_projects.collect {|p| [p.name, p.id]}), { :include_blank => true } %></p>
<% if User.current.admin? && !@project.possible_parents.empty? %>
<p><label><%= l(:field_parent) %></label><%= parent_project_select_tag(@project) %></p>
<% end %>
<p><%= f.text_area :description, :rows => 5, :class => 'wiki-edit' %></p>
......
......@@ -48,7 +48,7 @@
<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 />
<% 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>
<%= hidden_field_tag 'with_subprojects', 0 %>
<% end %>
......
......@@ -3,8 +3,8 @@
<p><strong><%=h @project_to_destroy %></strong><br />
<%=l(:text_project_destroy_confirmation)%>
<% if @project_to_destroy.children.any? %>
<br /><%= 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? %>
<br /><%= l(:text_subprojects_destroy_warning, content_tag('strong', h(@project_to_destroy.descendants.collect{|p| p.to_s}.join(', ')))) %>
<% end %>
</p>
<p>
......
......@@ -6,20 +6,11 @@
<h2><%=l(:label_project_plural)%></h2>
<% @project_tree.keys.sort.each do |project| %>
<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 %>
<%= render_project_hierarchy(@projects)%>
<% if User.current.logged? %>
<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>
<% end %>
......
......@@ -4,11 +4,13 @@
<%= textilizable @project.description %>
<ul>
<% unless @project.homepage.blank? %><li><%=l(:field_homepage)%>: <%= link_to(h(@project.homepage), @project.homepage) %></li><% end %>
<% if @subprojects.any? %>
<li><%=l(:label_subproject_plural)%>: <%= @subprojects.collect{|p| link_to(h(p.name), :action => 'show', :id => p)}.join(", ") %></li>
<% end %>
<% if @project.parent %>
<li><%=l(:field_parent)%>: <%= link_to h(@project.parent.name), :controller => 'projects', :action => 'show', :id => @project.parent %></li>
<% if @subprojects.any? %>
<li><%=l(:label_subproject_plural)%>:
<%= @subprojects.collect{|p| link_to(h(p), :action => 'show', :id => p)}.join(", ") %></li>
<% end %>
<% if @ancestors.any? %>
<li><%=l(:field_parent)%>:
<%= @ancestors.collect {|p| link_to(h(p), :action => 'show', :id => p)}.join(" &#187; ") %></li>
<% end %>
<% @project.custom_values.each do |custom_value| %>
<% if !custom_value.value.empty? %>
......
......@@ -31,7 +31,7 @@
<p>
<label><%=l(:label_project_new)%></label><br/>
<% 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) %>:
<%= select_tag 'membership[role_id]', options_from_collection_for_select(@roles, "id", "name") %>
<%= 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; }
table.list td.id { width: 2%; text-align: center;}
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 td.subject, tr.issue td.category, td.assigned_to { white-space: normal; }
tr.issue td.subject { text-align: left; }
......@@ -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 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 li {list-style-type:none;}
ul.properties li span {font-style:italic;}
......
......@@ -10,6 +10,8 @@ projects_001:
is_public: true
identifier: ecookbook
parent_id:
lft: 1
rgt: 10
projects_002:
created_on: 2006-07-19 19:14:19 +02:00
name: OnlineStore
......@@ -21,6 +23,8 @@ projects_002:
is_public: false
identifier: onlinestore
parent_id:
lft: 11
rgt: 12
projects_003:
created_on: 2006-07-19 19:15:21 +02:00
name: eCookbook Subproject 1
......@@ -32,6 +36,8 @@ projects_003:
is_public: true
identifier: subproject1
parent_id: 1
lft: 6
rgt: 7
projects_004:
created_on: 2006-07-19 19:15:51 +02:00
name: eCookbook Subproject 2
......@@ -43,6 +49,8 @@ projects_004:
is_public: true
identifier: subproject2
parent_id: 1