From 2ff4ef782229431ba40b718c10d8e53bd1ab5f41 Mon Sep 17 00:00:00 2001
From: ciaranj <ciaranj@gmail.com>
Date: Wed, 3 Feb 2010 10:23:41 +0000
Subject: [PATCH] Update-Of-Ported-the-redmine_subtasks-plugin-from-Aleksei-Guse.patch

---
 app/controllers/issues_controller.rb               |  119 +++++++-
 app/controllers/projects_controller.rb             |    2 +
 app/controllers/queries_controller.rb              |    1 +
 app/helpers/issues_helper.rb                       |   22 ++
 app/helpers/queries_helper.rb                      |   94 ++++++-
 app/helpers/versions_helper.rb                     |   28 ++
 app/models/issue.rb                                |  242 +++++++++++++-
 app/models/query.rb                                |   30 ++
 app/models/version.rb                              |    2 +
 app/models/view_option.rb                          |   20 ++
 app/views/issues/_action_menu.rhtml                |    8 +
 app/views/issues/_attributes.rhtml                 |   10 +
 app/views/issues/_list.rhtml                       |   44 ++-
 app/views/issues/_list_organized_by_parent.rhtml   |   14 +
 app/views/issues/_parent_field.rhtml               |   19 +
 app/views/issues/_subissues_list.rhtml             |   17 +
 app/views/issues/context_menu.rhtml                |    2 +-
 app/views/issues/index.rhtml                       |    6 +
 app/views/issues/show.rhtml                        |    6 +
 app/views/projects/roadmap.rhtml                   |    8 +-
 app/views/queries/_form.rhtml                      |    8 +
 app/views/settings/_issues.rhtml                   |    9 +
 app/views/versions/show.rhtml                      |   16 +-
 config/environment.rb                              |    2 +-
 config/locales/en.yml                              |   14 +
 config/settings.yml                                |   12 +
 .../20090115162651_add_queries_view_options.rb     |    9 +
 ...52_add_default_value_of_view_optoins_queries.rb |   11 +
 ...90406213813_add_issues_parent_id_lft_and_rgt.rb |   14 +
 db/migrate/20090406213899_issues_rebuild.rb        |   15 +
 ...20091211204929_add_lft_rgt_indexes_to_issues.rb |   11 +
 ...091211205222_add_indexes_to_issues_parent_id.rb |    9 +
 lib/redmine.rb                                     |    2 +-
 public/images/contract.png                         |  Bin 0 -> 290 bytes
 public/images/corner-dots.gif                      |  Bin 0 -> 59 bytes
 public/images/expand.png                           |  Bin 0 -> 2939 bytes
 public/javascripts/application.js                  |   21 ++-
 public/stylesheets/application.css                 |   41 +++
 test/fixtures/issues.yml                           |  169 ++++++++++
 test/fixtures/queries.yml                          |   27 ++
 test/fixtures/versions.yml                         |   19 +
 test/functional/issues_controller_test.rb          |  346 +++++++++++++++++++-
 test/functional/projects_controller_test.rb        |    6 +
 test/functional/queries_controller_test.rb         |   17 +
 test/integration/projects_test.rb                  |    3 +
 test/unit/enumeration_test.rb                      |    4 +-
 test/unit/issue_priority_test.rb                   |    2 +-
 test/unit/issue_test.rb                            |  314 ++++++++++++++++++
 test/unit/project_test.rb                          |   40 ++-
 test/unit/query_test.rb                            |    2 +-
 50 files changed, 1770 insertions(+), 67 deletions(-)
 create mode 100644 app/models/view_option.rb
 create mode 100644 app/views/issues/_list_organized_by_parent.rhtml
 create mode 100644 app/views/issues/_parent_field.rhtml
 create mode 100644 app/views/issues/_subissues_list.rhtml
 create mode 100644 db/migrate/20090115162651_add_queries_view_options.rb
 create mode 100644 db/migrate/20090115162652_add_default_value_of_view_optoins_queries.rb
 create mode 100644 db/migrate/20090406213813_add_issues_parent_id_lft_and_rgt.rb
 create mode 100644 db/migrate/20090406213899_issues_rebuild.rb
 create mode 100644 db/migrate/20091211204929_add_lft_rgt_indexes_to_issues.rb
 create mode 100644 db/migrate/20091211205222_add_indexes_to_issues_parent_id.rb
 create mode 100644 public/images/contract.png
 create mode 100644 public/images/corner-dots.gif
 create mode 100644 public/images/expand.png

diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb
index 2e0ed6f..0455d0e 100644
--- a/app/controllers/issues_controller.rb
+++ b/app/controllers/issues_controller.rb
@@ -21,9 +21,12 @@ class IssuesController < ApplicationController
   
   before_filter :find_issue, :only => [:show, :edit, :reply]
   before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
-  before_filter :find_project, :only => [:new, :update_form, :preview]
-  before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu]
+  before_filter :find_project, :only => [:new, :update_form, :preview, :add_subissue, :auto_complete_for_issue_parent]
+  before_filter :authorize, :except => [:index, :changes, :gantt, :calendar, :preview, :context_menu, :add_subissue]
   before_filter :find_optional_project, :only => [:index, :changes, :gantt, :calendar]
+  before_filter :find_parent_issue, :only => [:add_subissue]
+  before_filter :find_optional_parent_issue, :only => [:new, :update_form]
+
   accept_key_auth :index, :show, :changes
 
   rescue_from Query::StatementInvalid, :with => :query_statement_invalid
@@ -45,6 +48,7 @@ class IssuesController < ApplicationController
   include IssuesHelper
   helper :timelog
   include Redmine::Export::PDF
+  include ActionView::Helpers::PrototypeHelper
 
   verify :method => :post,
          :only => :destroy,
@@ -102,6 +106,7 @@ class IssuesController < ApplicationController
   end
   
   def show
+    retrieve_query_for_subissues
     @journals = @issue.journals.find(:all, :include => [:user, :details], :order => "#{Journal.table_name}.created_on ASC")
     @journals.each_with_index {|j,i| j.indice = i+1}
     @journals.reverse! if User.current.wants_comments_in_reverse_order?
@@ -151,6 +156,7 @@ class IssuesController < ApplicationController
       # Check that the user is allowed to apply the requested status
       @issue.status = (@allowed_statuses.include? requested_status) ? requested_status : default_status
       call_hook(:controller_issues_new_before_save, { :params => params, :issue => @issue })
+      @issue.parent_id = params[:issue][:parent_id] if params[:issue]
       if @issue.save
         attach_files(@issue, params[:attachments])
         flash[:notice] = l(:notice_successful_create)
@@ -185,6 +191,8 @@ class IssuesController < ApplicationController
     end
 
     if request.post?
+      @issue.parent_id = params[:issue][:parent_id] if params[:issue] && params[:issue][:parent_id]
+
       @time_entry = TimeEntry.new(:project => @project, :issue => @issue, :user => User.current, :spent_on => Date.today)
       @time_entry.attributes = params[:time_entry]
       attachments = attach_files(@issue, params[:attachments])
@@ -212,6 +220,12 @@ class IssuesController < ApplicationController
     attachments.each(&:destroy)
   end
 
+  def add_subissue
+    redirect_to :action => 'new',
+                :project_id => @parent_issue.project,
+                :issue => { :parent_id => @parent_issue.id }
+  end
+  
   def reply
     journal = Journal.find(params[:journal_id]) if params[:journal_id]
     if journal
@@ -369,10 +383,25 @@ class IssuesController < ApplicationController
                               :order => "start_date, effective_date",
                               :conditions => ["(((start_date>=? and start_date<=?) or (effective_date>=? and effective_date<=?) or (start_date<? and effective_date>?)) and start_date is not null and due_date is null and effective_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
                               )
+      # Parent issues that might not have the due_date set but do have
+      # child issues that do have due_date set should be included.
+      events.each do |issue|
+        if issue.leaf?
+          # Can't use the Issue#visible named_scope because it causes
+          # a SQL error with the awesome_nested_set
+          ancestors = issue.ancestors.all(:include => [:tracker, :assigned_to, :priority, :project],
+                                          :order => "start_date",
+                                          :conditions => 'start_date IS NOT NULL')
+          ancestors.map! {|i| i.visible? ? i : nil }.compact!
+
+          events += ancestors.flatten if ancestors.present?
+        end
+      end
+      
       # Versions
       events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
-                                   
-      @gantt.events = events
+
+      @gantt.events = events.uniq
     end
     
     basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
@@ -458,14 +487,72 @@ class IssuesController < ApplicationController
     @text = params[:notes] || (params[:issue] ? params[:issue][:description] : nil)
     render :partial => 'common/preview'
   end
+
+  def auto_complete_for_issue_parent
+    @phrase = params[:issue_parent]
+    @candidates = []
+
+    # If cross project issue relations is allowed we should get
+    # candidates from every project
+    if Setting.cross_project_issue_relations?
+      projects_to_search = nil
+    else
+      projects_to_search = [ @project ] + @project.children
+    end
+
+    if @phrase.present?
+      # Try to find issue by id.
+      if @phrase.match(/^#?(\d+)$/)
+        if Setting.cross_project_issue_relations?
+          issue = Issue.visible.find_by_id( $1)
+        else
+          issue = Issue.visible.find_by_id_and_project_id( $1, projects_to_search.collect { |i| i.id})
+        end
+        @candidates << issue if issue
+      end
+
+      # Search by subject and description
+      # extract tokens from the question
+      # eg. hello "bye bye" => ["hello", "bye bye"]
+      tokens = @phrase.scan(%r{((\s|^)"[\s\w]+"(\s|$)|\S+)}).collect {|m| m.first.gsub(%r{(^\s*"\s*|\s*"\s*$)}, '')}
+      # tokens must be at least 3 character long
+      tokens = tokens.uniq.select {|w| w.length > 2 }
+      like_tokens = tokens.collect {|w| "%#{w.downcase}%"}
+
+      search_results, count = Issue.search( like_tokens, projects_to_search, :before => true, :limit => 10)
+      @candidates += search_results unless search_results.empty?
+    end
+
+    # Remove the current issue if it's a result
+    if params[:id].present?
+      @issue = Issue.visible.find_by_id(params[:id]) 
+      @candidates.delete(@issue)
+    end
+
+    render :inline => "<%= auto_complete_result_parent_issue( @candidates, @phrase) %>"
+  end
   
 private
   def find_issue
     @issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
     @project = @issue.project
+    @parent_issue = @issue.parent if @issue
+  rescue ActiveRecord::RecordNotFound
+    render_404
+  end
+
+  def find_parent_issue
+    @parent_issue = Issue.find( params[:parent_issue_id])
+    render_404 unless @parent_issue.visible?(User.current)
   rescue ActiveRecord::RecordNotFound
     render_404
   end
+
+  def find_optional_parent_issue
+    if params[:issue] && !params[:issue][:parent_id].blank?
+      @parent_issue = Issue.visible.find_by_id( params[:issue][:parent_id])
+    end
+  end
   
   # Filter for bulk operations
   def find_issues
@@ -522,14 +609,36 @@ private
         end
         @query.group_by = params[:group_by]
         @query.column_names = params[:query] && params[:query][:column_names]
-        session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names}
+        if params[:view_options] and params[:view_options].is_a? Hash
+          params[:view_options].each_pair do |name, value|
+            @query.set_view_option( name, value)
+          end
+        end
+        
+        session[:query] = {:project_id => @query.project_id, :filters => @query.filters, :group_by => @query.group_by, :column_names => @query.column_names, :view_options => @query.view_options}
       else
         @query = Query.find_by_id(session[:query][:id]) if session[:query][:id]
         @query ||= Query.new(:name => "_", :project => @project, :filters => session[:query][:filters], :group_by => session[:query][:group_by], :column_names => session[:query][:column_names])
+        if session[:query][:view_options]
+          session[:query][:view_options].each_pair do |name, value|
+            @query.set_view_option( name, value)
+          end
+        end
         @query.project = @project
       end
     end
   end
+
+  # Retrive and build a query for the subissues
+  def retrieve_query_for_subissues
+    retrieve_query
+    @query.project = @project
+    @query.set_view_option('show_parents', ViewOption::SHOW_PARENTS[:organize_by])
+    @query.column_names = Setting.subissues_list_columns
+    sort_init( @query.sort_criteria.empty? ? [['id', 'desc']] : @query.sort_criteria)
+    sort_update({'id' => "#{Issue.table_name}.id"}.merge( @query.available_columns.inject({}) { |h, c| h[c.name.to_s] = c.sortable; h}))
+
+  end
   
   # Rescues an invalid query statement. Just in case...
   def query_statement_invalid(exception)
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index e908388..98a54ed 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -46,6 +46,8 @@ class ProjectsController < ApplicationController
   helper :repositories
   include RepositoriesHelper
   include ProjectsHelper
+  helper :versions
+  include VersionsHelper
   
   # Lists visible projects
   def index
diff --git a/app/controllers/queries_controller.rb b/app/controllers/queries_controller.rb
index 16755a1..93b33c2 100644
--- a/app/controllers/queries_controller.rb
+++ b/app/controllers/queries_controller.rb
@@ -31,6 +31,7 @@ class QueriesController < ApplicationController
       @query.add_filter(field, params[:operators][field], params[:values][field])
     end if params[:fields]
     @query.group_by ||= params[:group_by]
+    @query.view_options = params[:view_options] if params[:view_options]
     
     if request.post? && params[:confirm] && @query.save
       flash[:notice] = l(:notice_successful_create)
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 1f74011..8aec012 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -96,6 +96,15 @@ module IssuesHelper
       when 'estimated_hours'
         value = "%0.02f" % detail.value.to_f unless detail.value.blank?
         old_value = "%0.02f" % detail.old_value.to_f unless detail.old_value.blank?
+      when 'parent_id'
+        if detail.value && i = Issue.visible.find_by_id(detail.value)
+          value = i.subject
+        end
+
+        if detail.old_value && i = Issue.visible.find_by_id(detail.old_value)
+          old_value = i.subject
+        end
+        label = l(:field_parent_issue)
       end
     when 'cf'
       custom_field = CustomField.find_by_id(detail.prop_key)
@@ -196,4 +205,17 @@ module IssuesHelper
     end
     export
   end
+
+  def auto_complete_result_parent_issue(candidates, phrase)
+    if candidates.present?
+      candidates.map! do |c|
+        content_tag("li",
+                    highlight( c.to_s, phrase),
+                    :id => String( c[:id]))
+      end
+    else
+      candidates = [content_tag(:li, l(:label_none), :style => 'display:none')]
+    end
+    content_tag("ul", candidates.uniq)
+  end
 end
diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb
index ecfac55..66ac7ba 100644
--- a/app/helpers/queries_helper.rb
+++ b/app/helpers/queries_helper.rb
@@ -27,13 +27,13 @@ module QueriesHelper
                       content_tag('th', column.caption)
   end
   
-  def column_content(column, issue)
+  def column_content(column, issue, query = nil)
     value = column.value(issue)
     
     case value.class.name
     when 'String'
       if column.name == :subject
-        link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
+        subject_in_tree( issue, issue.subject, query)
       else
         h(value)
       end
@@ -61,4 +61,94 @@ module QueriesHelper
       h(value)
     end
   end
+
+  def subject_in_tree(issue, value, query)
+    if query.view_options['show_parents'] == ViewOption::SHOW_PARENTS[:never]
+      content_tag('div', subject_text(issue, value), :class=>'issue-subject')
+    else
+      css_style = "margin-left: #{issue.level}em;" # Used to indent
+      content_tag('span',
+                  content_tag('div',
+                              subject_text(issue, value),
+                              :class=>'issue-subject',
+                              :style => css_style),
+                  :class => issue.level > 0 ? "issue-subject-in-tree issue-level-#{issue.level}" : '',
+                  :style => css_style)
+    end
+  end
+  
+  def subject_text(issue, value)
+    if issue.visible?
+      subject_text = link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
+      h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + subject_text
+    else
+      h(value)
+    end
+  end
+
+  def issue_content(issue, query, options = { })
+    row_classes = ['issue','hascontextmenu', issue.css_classes, cycle('odd', 'even')]
+    row_classes << 'issue-unfiltered' if options[:unfiltered]
+    row_classes << 'issue-emphasis' if options[:emphasis]
+
+    inner_content = returning '' do |content|
+      content << content_tag(:td, check_box_tag("ids[]", issue.id, false, :id => nil), :class => 'checkbox')
+      content << content_tag(:td, link_to(issue.id, :controller => 'issues', :action => 'show', :id => issue))
+
+      query.columns.each do |column|
+        content << content_tag( 'td', column_content(column, issue, query), :class => column.name)
+      end
+    end
+    
+    content_tag(:tr,
+                inner_content,
+                :id => "issue-#{issue.id}",
+                :class => row_classes.join(' '))
+  end
+
+  def private_issue_content(issue, query, options = { })
+    row_classes = ['issue', 'private-issue',cycle('odd', 'even')]
+    row_classes << 'issue-unfiltered' if options[:unfiltered]
+    row_classes << 'issue-emphasis' if options[:emphasis]
+
+    inner_content = returning '' do |content|
+      content << content_tag(:td, check_box_tag("ids[]", '', false, :id => nil), :class => 'checkbox')
+      content << content_tag(:td, l(:text_private))
+
+      query.columns.each do |column|
+        if column.name == :subject
+          # Need to indent
+          content << content_tag('td', subject_in_tree(issue, l(:text_private), query), :class => column.name)
+        else
+          content << content_tag( 'td', l(:text_private), :class => column.name)
+        end
+      end
+    end
+    
+    content_tag(:tr,
+                inner_content,
+                :id => "",
+                :class => row_classes.join(' '))
+    
+  end
+
+  def issues_family_content( parent, issues_to_show, query, emphasis_issues)
+    html = ""
+    if parent.visible?
+      html << issue_content( parent, query, :unfiltered => !( issues_to_show.include? parent),
+                             :emphasis => ( emphasis_issues ? emphasis_issues.include?( parent) : false))
+    else
+      html << private_issue_content( parent, query, :unfiltered => !( issues_to_show.include? parent),
+                                     :emphasis => ( emphasis_issues ? emphasis_issues.include?( parent) : false))
+    end
+    unless  parent.children.empty?
+      parent.children.each do |child|
+        if issues_to_show.include?( child) || issues_to_show.detect { |i| i.ancestors.include? child }
+          html << issues_family_content( child, issues_to_show, query, emphasis_issues)
+        end
+      end
+    end
+    html
+  end
+
 end
diff --git a/app/helpers/versions_helper.rb b/app/helpers/versions_helper.rb
index 0fcc640..1b15c1a 100644
--- a/app/helpers/versions_helper.rb
+++ b/app/helpers/versions_helper.rb
@@ -44,4 +44,32 @@ module VersionsHelper
   def status_by_options_for_select(value)
     options_for_select(STATUS_BY_CRITERIAS.collect {|criteria| [l("field_#{criteria}".to_sym), criteria]}, value)
   end
+
+  def render_list_of_related_issues( issues, version, current_level = 0)
+    issues_on_current_level = issues.select { |i| i.level == current_level }
+    issues -= issues_on_current_level
+    content_tag( 'ul') do
+      html = ''
+      issues_on_current_level.each do |issue|
+        opts_for_issue_li = { }
+        if !issue.fixed_version or issue.fixed_version != version
+          opts_for_issue_li[:class] = 'issue-unfiltered'
+        end
+        html << content_tag( 'li', opts_for_issue_li) do
+          opts = { }
+          if issue.done_ratio == 100
+            opts[:style] = 'font-weight: bold'
+          end
+          link_to_issue(issue, opts)
+        end
+        children_to_print = issues & issue.children
+        children_to_print += issues.select { |i| i.level >= current_level + 2}
+        unless children_to_print.empty?
+          html << render_list_of_related_issues( children_to_print, version, current_level + 1)
+        end
+      end
+      html
+    end
+  end
+
 end
diff --git a/app/models/issue.rb b/app/models/issue.rb
index b9e0461..1602a75 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -36,7 +36,7 @@ class Issue < ActiveRecord::Base
   acts_as_customizable
   acts_as_watchable
   acts_as_searchable :columns => ['subject', "#{table_name}.description", "#{Journal.table_name}.notes"],
-                     :include => [:project, :journals],
+                     :include => [:project, :journals, :tracker],
                      # sort by id so that limited eager loading doesn't break with postgresql
                      :order_column => "#{table_name}.id"
   acts_as_event :title => Proc.new {|o| "#{o.tracker.name} ##{o.id} (#{o.status}): #{o.subject}"},
@@ -46,12 +46,37 @@ class Issue < ActiveRecord::Base
   acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]},
                             :author_key => :author_id
 
-  DONE_RATIO_OPTIONS = %w(issue_field issue_status)
+  # Needs to be registered before any before_destroy in acts_as_nested_set
+  before_destroy :move_children_to_root_before_destroy
+
+  acts_as_nested_set
+
+  # Patches to acts_as_nested_set since Issue already defines #move_to
+  def move_to_left_of(node)
+    nested_set_move_to node, :left
+  end
+
+  def move_to_right_of(node)
+    nested_set_move_to node, :right
+  end
+
+  def move_to_child_of(node)
+    nested_set_move_to node, :child
+  end
+  
+  def move_to_root
+    nested_set_move_to nil, :root
+  end
+
+  alias_method :nested_set_move_to, :move_to
   
+  DONE_RATIO_OPTIONS = %w(issue_field issue_status)
+
   validates_presence_of :subject, :priority, :project, :tracker, :author, :status
   validates_length_of :subject, :maximum => 255
   validates_inclusion_of :done_ratio, :in => 0..100
   validates_numericality_of :estimated_hours, :allow_nil => true
+  validate :subtasks_validation
 
   named_scope :visible, lambda {|*args| { :include => :project,
                                           :conditions => Project.allowed_to_condition(args.first || User.current, :view_issues) } }
@@ -60,6 +85,8 @@ class Issue < ActiveRecord::Base
 
   before_save :update_done_ratio_from_issue_status
   after_save :create_journal
+  after_save :set_parent
+  after_save :do_subtasks_hooks
   
   # Returns true if usr or current user is allowed to view the issue
   def visible?(usr=nil)
@@ -91,13 +118,20 @@ class Issue < ActiveRecord::Base
   # Returns the moved/copied issue on success, false on failure
   def move_to(new_project, new_tracker = nil, options = {})
     options ||= {}
-    issue = options[:copy] ? self.clone : self
+    issue = if options[:copy]
+              Issue.new( self.attributes.reject { |k,v| k == 'parent_id' })
+            else
+              self
+            end
+
     transaction do
       if new_project && issue.project_id != new_project.id
         # delete issue relations
         unless Setting.cross_project_issue_relations?
           issue.relations_from.clear
           issue.relations_to.clear
+
+          issue.children.each(&:move_to_root) unless options[:copy]
         end
         # issue is moved to another project
         # reassign to the category with same name if any
@@ -129,6 +163,9 @@ class Issue < ActiveRecord::Base
           # Manually update project_id on related time entries
           TimeEntry.update_all("project_id = #{new_project.id}", {:issue_id => id})
         end
+        if new_project && issue.project_id != new_project.id && !Setting.cross_project_issue_relations?
+          issue.move_to_root
+        end
       else
         Issue.connection.rollback_db_transaction
         return false
@@ -136,7 +173,16 @@ class Issue < ActiveRecord::Base
     end
     return issue
   end
-  
+
+  # Cache awesome_nested_set's level attribute, it goes back to the
+  # database and counts ancestors which can be expensive.
+  def level
+    unless @level
+      @level = super
+    end
+    @level
+  end
+
   def priority_id=(pid)
     self.priority = nil
     write_attribute(:priority_id, pid)
@@ -160,16 +206,90 @@ class Issue < ActiveRecord::Base
     self.attributes_without_tracker_first = new_attributes, *args
   end
   alias_method_chain :attributes=, :tracker_first
+
+  # Need to define the setter because awesome_nested_set removes the
+  # parent_id setter since parent is an internal field.  If parent
+  # isn't set though, then parent changes will not be logged to journals.
+  def parent_id=(pid)
+    if pid != id
+      write_attribute(:parent_id, pid)
+    else
+      false # Circular reference
+    end
+  end
+
+  def estimated_hours
+    if leaf?
+      read_attribute(:estimated_hours)
+    else
+      children.inject(0) do |sum, issue|
+        if issue.estimated_hours.present?
+          sum + issue.estimated_hours
+        else
+          sum
+        end
+      end
+    end
+  end
+
+  # Returns the estimated_hours, disregarding child issues
+  def original_estimated_hours
+    read_attribute(:estimated_hours)
+  end
   
   def estimated_hours=(h)
-    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
+    write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h) if leaf?
+  end
+
+  def due_date
+    if leaf?
+      read_attribute( :due_date)
+    else
+      unless @due_date # cache, expensive operation
+        dates = leaves.map(&:due_date)
+        @due_date = dates.select {|d| d }.max if (dates && dates.any?)
+      end
+      @due_date
+    end
+  end  
+  
+  [ :due_date, :done_ratio ].each do |method|
+    src = <<-END_SRC
+      def #{method}=(value)
+        write_attribute( :#{method}, value) if leaf?
+      end
+      END_SRC
+    class_eval src, __FILE__, __LINE__
   end
   
   def done_ratio
-    if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
-      status.default_done_ratio
+    if leaf? 
+      if Issue.use_status_for_done_ratio? && status && status.default_done_ratio?
+        status.default_done_ratio
+      else
+        read_attribute(:done_ratio)
+      end
     else
-      read_attribute(:done_ratio)
+      unless @done_ratio # cache, expensive operation
+        if Issue.use_status_for_done_ratio?
+          total_done_ratio=children.inject(0) {|sum, i| sum + i.done_ratio}
+          if total_done_ratio == 0 
+            @done_ratio = 0
+          else 
+            @done_ratio = (total_done_ratio / children.size )
+          end
+        else 
+          total_planned_days = leaves.inject(0) {|sum,i| sum + i.duration}
+
+          if total_planned_days == 0
+            @done_ratio = 0
+          else
+            total_actual_days = leaves.inject(0) {|sum,i| sum + i.actual_days}
+            @done_ratio = (total_actual_days * 100 / total_planned_days).floor
+          end
+        end
+      end
+      @done_ratio
     end
   end
 
@@ -182,7 +302,7 @@ class Issue < ActiveRecord::Base
   end
   
   def validate
-    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty?
+    if self.due_date.nil? && @attributes['due_date'] && !@attributes['due_date'].empty? && leaf?
       errors.add :due_date, :not_a_date
     end
     
@@ -231,7 +351,13 @@ class Issue < ActiveRecord::Base
     
     # Update start/due dates of following issues
     relations_from.each(&:set_issue_to_dates)
-    
+
+    # If target version is set, but "Due to" date is not, set
+    # it as the same as the date of target version.
+    if leaf? && due_date.nil? && fixed_version && fixed_version.due_date
+      self.update_attribute :due_date, fixed_version.due_date
+    end
+
     # Close duplicates if the issue was closed
     if @issue_before_change && !@issue_before_change.closed? && self.closed?
       duplicates.each do |duplicate|
@@ -256,11 +382,19 @@ class Issue < ActiveRecord::Base
     updated_on_will_change!
     @current_journal
   end
+
+  def journal_initilized?
+    @current_journal
+  end
   
   # Return true if the issue is closed, otherwise false
   def closed?
     self.status.is_closed?
   end
+
+  def open?
+    !closed?
+  end
   
   # Return true if the issue is being reopened
   def reopened?
@@ -359,6 +493,17 @@ class Issue < ActiveRecord::Base
   def soonest_start
     @soonest_start ||= relations_to.collect{|relation| relation.successor_soonest_start}.compact.min
   end
+
+  # Returns the number of days that have been worked on this issue.
+  # Calculated by using the duration of the issue (start/end dates)
+  # and the done ratio
+  def actual_days
+    if done_ratio
+      (duration * done_ratio / 100).floor
+    else
+      0
+    end
+  end
   
   def to_s
     "#{tracker} ##{id}: #{subject}"
@@ -388,6 +533,10 @@ class Issue < ActiveRecord::Base
     Issue.update_versions(["#{Version.table_name}.project_id IN (?) OR #{Issue.table_name}.project_id IN (?)", moved_project_ids, moved_project_ids])
   end
 
+  def leaf?
+    new_record? || (right - left == 1)
+  end
+
   private
   
   # Update issues so their versions are not pointing to a
@@ -402,7 +551,7 @@ class Issue < ActiveRecord::Base
               :include => [:project, :fixed_version]
               ).each do |issue|
       next if issue.project.nil? || issue.fixed_version.nil?
-      unless issue.project.shared_versions.include?(issue.fixed_version)
+      unless issue.project.shared_versions.collect(&:id).include?(issue.fixed_version_id)
         issue.init_journal(User.current)
         issue.fixed_version = nil
         issue.save
@@ -424,7 +573,11 @@ class Issue < ActiveRecord::Base
   def create_journal
     if @current_journal
       # attributes changes
-      (Issue.column_names - %w(id description lock_version created_on updated_on)).each {|c|
+      skip_attrs = %w(id description lock_version created_on updated_on)
+      skip_attrs += %w(due_date done_ratio estimated_hours) unless leaf?
+
+      # attributes changes
+      (Issue.column_names - skip_attrs).each {|c|
         @current_journal.details << JournalDetail.new(:property => 'attr',
                                                       :prop_key => c,
                                                       :old_value => @issue_before_change.send(c),
@@ -442,4 +595,69 @@ class Issue < ActiveRecord::Base
       @current_journal.save
     end
   end
+
+
+  def move_children_to_root_before_destroy
+    unless Setting.delete_children?
+      children.each( &:move_to_root)
+      reload_nested_set
+    end
+  end
+  
+  def do_subtasks_hooks
+    if parent
+      # Need to reload the Issues.  Using the association or
+      # parent.reload was keeping the object readonly.
+      parent_issue = Issue.find parent.id
+      self.reload
+
+      # Update the parent status if this issue is open and the parent
+      # is closed
+      if open? && parent_issue.closed?
+        parent_issue.init_journal(User.current)
+        parent_issue.status = IssueStatus.find_by_id(Setting.reopened_parent_issue_status) || IssueStatus.default
+      end
+      
+      # Set 'Target version' of parent if one was set on one of the
+      # children issue and parent have no 'Target version'. Do the same
+      # if 'Target version of the parent issue lower (by the release
+      # date or by the version number).
+      if parent_issue.fixed_version.nil? && fixed_version or
+          ( parent_issue.fixed_version && fixed_version and
+            parent_issue.fixed_version.project == fixed_version.project and
+            parent_issue.fixed_version < fixed_version )
+        parent_issue.init_journal(User.current) unless parent_issue.journal_initilized?
+        parent_issue.fixed_version = fixed_version
+      end
+      parent_issue.save if parent_issue.changed?
+    end
+  end
+
+  def set_parent
+    if (@issue_before_change && @issue_before_change.parent_id != parent_id) ||
+        self.lock_version == 0 # Newly saved record
+      if parent_id.present?
+        parent_issue = Issue.visible.find_by_id(parent_id)
+        move_to_child_of parent_issue if parent_issue
+      else
+        move_to_root
+      end
+    end
+  end
+  
+  def subtasks_validation
+    unless children.empty?
+      if IssueStatus.find_by_id( @attributes['status_id']).is_closed? && children.detect { |i| !i.closed? }
+        errors.add( :status, l(:error_issue_subtasks_cant_close_parent))
+      end
+      
+      children_max_fixed_version = children.select { |i| i.fixed_version } .max { |a,b| a.fixed_version <=> b.fixed_version }
+      if @attributes['fixed_version_id'] && children_max_fixed_version
+        if Version.find_by_id( @attributes['fixed_version_id']) < children_max_fixed_version.fixed_version
+          errors.add :fixed_version, l(:error_issue_subtasks_cant_select_lower_target_version)
+        end
+      end
+    end
+  end
+
 end
diff --git a/app/models/query.rb b/app/models/query.rb
index afbb687..892a8b3 100644
--- a/app/models/query.rb
+++ b/app/models/query.rb
@@ -78,6 +78,7 @@ class Query < ActiveRecord::Base
   serialize :filters
   serialize :column_names
   serialize :sort_criteria, Array
+  serialize :view_options
   
   attr_protected :project_id, :user_id
   
@@ -135,10 +136,23 @@ class Query < ActiveRecord::Base
     QueryColumn.new(:created_on, :sortable => "#{Issue.table_name}.created_on", :default_order => 'desc'),
   ]
   cattr_reader :available_columns
+
+  @@available_view_options =
+    [ ViewOption.new( 'show_parents',
+                      [ [ l(:label_view_option_parents_do_not_show), 
+                          ViewOption::SHOW_PARENTS[:never] ],
+                        [ l(:label_view_option_parents_show_always), 
+                          ViewOption::SHOW_PARENTS[:always] ],
+                        [ l(:label_view_option_parents_show_and_group), 
+                          ViewOption::SHOW_PARENTS[:organize_by]]])
+    ]
+  cattr_reader :available_view_options
+
   
   def initialize(attributes = nil)
     super attributes
     self.filters ||= { 'status_id' => {:operator => "o", :values => [""]} }
+    self.view_options ||=  { 'show_parents' => 'do_not_show' }
   end
   
   def after_initialize
@@ -470,6 +484,22 @@ class Query < ActiveRecord::Base
   rescue ::ActiveRecord::StatementInvalid => e
     raise StatementInvalid.new(e.message)
   end
+
+  def set_view_option( option, value)
+    self.view_options[option] = value
+    # Clear group_by if organize_by_parent is selected
+    if option == 'show_parents' && value == 'organize_by_parent'
+      self.group_by = nil
+    end
+  end
+
+  def values_for_view_option( option)
+    @@available_view_options.find { |vo| vo.name == option }.available_values
+  end
+
+  def caption_for_view_option( option)
+    @@available_view_options.find { |vo| vo.name == option }.caption
+  end
   
   private
   
diff --git a/app/models/version.rb b/app/models/version.rb
index bc0e17e..6436248 100644
--- a/app/models/version.rb
+++ b/app/models/version.rb
@@ -16,6 +16,8 @@
 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.
 
 class Version < ActiveRecord::Base
+  include Comparable
+
   before_destroy :check_integrity
   after_update :update_issues_from_sharing_change
   belongs_to :project
diff --git a/app/models/view_option.rb b/app/models/view_option.rb
new file mode 100644
index 0000000..efde729
--- /dev/null
+++ b/app/models/view_option.rb
@@ -0,0 +1,20 @@
+class ViewOption
+  attr_accessor :name, :available_values
+  include Redmine::I18n
+
+  unless const_defined? :SHOW_PARENTS
+    SHOW_PARENTS = { :never       => 'do_not_show',
+                     :always      => 'show_always',
+                     :organize_by => 'organize_by_parent'}.freeze
+  end
+  
+  def initialize( name, available_values)
+    self.name = name
+    self.available_values = available_values
+  end
+
+  def caption
+    l("label_view_option_#{name}")
+  end
+end
+
diff --git a/app/views/issues/_action_menu.rhtml b/app/views/issues/_action_menu.rhtml
index 693b492..96df1ca 100644
--- a/app/views/issues/_action_menu.rhtml
+++ b/app/views/issues/_action_menu.rhtml
@@ -1,5 +1,13 @@
 <div class="contextual">
 <%= link_to_if_authorized(l(:button_update), {:controller => 'issues', :action => 'edit', :id => @issue }, :onclick => 'showAndScrollTo("update", "notes"); return false;', :class => 'icon icon-edit', :accesskey => accesskey(:edit)) %>
+<%= link_to_if_authorized(l(:button_add_subissue),
+                          {
+                          :controller => 'issues',
+                          :action => 'add_subissue',
+                          :project_id => @project,
+                          :parent_issue_id => @issue.id
+                        },
+                        :class => 'icon icon-add') %>
 <%= link_to_if_authorized l(:button_log_time), {:controller => 'timelog', :action => 'edit', :issue_id => @issue}, :class => 'icon icon-time-add' %>
 <% replace_watcher ||= 'watcher' %>
 <%= watcher_tag(@issue, User.current, {:id => replace_watcher, :replace => ['watcher','watcher2']}) %>
diff --git a/app/views/issues/_attributes.rhtml b/app/views/issues/_attributes.rhtml
index f8fc8d6..21928fe 100644
--- a/app/views/issues/_attributes.rhtml
+++ b/app/views/issues/_attributes.rhtml
@@ -31,12 +31,22 @@
 </div>
 
 <div class="splitcontentright">
+<% if @issue.new_record? ||  @issue.leaf? %>	
 <p><%= f.text_field :start_date, :size => 10 %><%= calendar_for('issue_start_date') %></p>
 <p><%= f.text_field :due_date, :size => 10 %><%= calendar_for('issue_due_date') %></p>
 <p><%= f.text_field :estimated_hours, :size => 3 %> <%= l(:field_hours) %></p>
 <% if Issue.use_field_for_done_ratio? %>
 <p><%= f.select :done_ratio, ((0..10).to_a.collect {|r| ["#{r*10} %", r*10] }) %></p>
 <% end %>
+<% else %>
+<p><label><%= l(:field_start_date) %></label> <%= format_date(@issue.start_date) %></p>
+<p><label><%= l(:field_due_date) %></label> <%= format_date(@issue.due_date) %></p>
+<p><label><%= l(:field_done_ratio) %></label> <%= "#{@issue.done_ratio}%" %></p>
+<% end %>
+</div>
+
+<div>
+<%= render :partial => 'parent_field' %>
 </div>
 
 <div style="clear:both;"> </div>
diff --git a/app/views/issues/_list.rhtml b/app/views/issues/_list.rhtml
index a7a7b06..0cee648 100644
--- a/app/views/issues/_list.rhtml
+++ b/app/views/issues/_list.rhtml
@@ -12,23 +12,33 @@
 	</tr></thead>
 	<% previous_group = false %>
 	<tbody>
-	<% issues.each do |issue| -%>
-  <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
-    <% reset_cycle %>
-    <tr class="group open">
-    	<td colspan="<%= query.columns.size + 2 %>">
-    		<span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
-      	<%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
-    	</td>
-		</tr>
-		<% previous_group = group %>
-  <% end %>
-	<tr id="issue-<%= issue.id %>" class="hascontextmenu <%= cycle('odd', 'even') %> <%= issue.css_classes %>">
-	    <td class="checkbox"><%= check_box_tag("ids[]", issue.id, false, :id => nil) %></td>
-		<td><%= link_to issue.id, :controller => 'issues', :action => 'show', :id => issue %></td>
-        <% query.columns.each do |column| %><%= content_tag 'td', column_content(column, issue), :class => column.name %><% end %>
-	</tr>
-	<% end -%>
+      <% emphasis_issues ||= [] %>
+      <% if query.view_options['show_parents'] == ViewOption::SHOW_PARENTS[:organize_by] -%>
+        <%= render :partial => 'list_organized_by_parent', :locals => { :issues => issues, :query => query, :emphasis_issues => emphasis_issues }%>
+      <% else %>
+      <% issues.each do |issue| -%>
+        <% if @query.grouped? && (group = @query.group_by_column.value(issue)) != previous_group %>
+          <% reset_cycle %>
+          <tr class="group open">
+            <td colspan="<%= query.columns.size + 2 %>">
+    		  <span class="expander" onclick="toggleRowGroup(this); return false;">&nbsp;</span>
+              <%= group.blank? ? 'None' : column_content(@query.group_by_column, issue) %> <span class="count">(<%= @issue_count_by_group[group] %>)</span>
+            </td>
+          </tr>
+          <% previous_group = group %>
+        <% end %>
+        <% if query.view_options['show_parents'] == ViewOption::SHOW_PARENTS[:always] -%>
+          <% issue.ancestors.each do |parent_issue| -%>
+            <% if parent_issue.visible? %>
+              <%= issue_content( parent_issue, query, :unfiltered => true) %>
+            <% else %>
+              <%= private_issue_content( parent_issue, query, :unfiltered => true) %>
+            <% end %>
+          <% end -%>
+        <% end %>
+        <%= issue_content( issue, query, :emphasis => ( emphasis_issues ? emphasis_issues.include?( issue) : false)) %>
+      <% end -%>
+      <% end -%>
 	</tbody>
 </table>
 <% end -%>
diff --git a/app/views/issues/_list_organized_by_parent.rhtml b/app/views/issues/_list_organized_by_parent.rhtml
new file mode 100644
index 0000000..9c95833
--- /dev/null
+++ b/app/views/issues/_list_organized_by_parent.rhtml
@@ -0,0 +1,14 @@
+<%-
+parents_on_first_lvl = []
+issues.each do |i|
+  if i.parent
+    first_parent = i.root
+  else
+    first_parent = i
+  end
+  parents_on_first_lvl += [ first_parent ] unless parents_on_first_lvl.include?( first_parent)
+end
+
+parents_on_first_lvl.each do |parent| -%>
+<%= issues_family_content( parent, issues, query, emphasis_issues) %>
+<% end -%>
diff --git a/app/views/issues/_parent_field.rhtml b/app/views/issues/_parent_field.rhtml
new file mode 100644
index 0000000..e36b59d
--- /dev/null
+++ b/app/views/issues/_parent_field.rhtml
@@ -0,0 +1,19 @@
+<%= hidden_field_tag('issue[parent_id]', (@parent_issue ? @parent_issue.id : ""), :id => :issue_parent_id) %>
+<p><label><%= l(:field_parent_issue) %></label>
+<% if authorize_for( 'issues', 'auto_complete_for_issue_parent') %>
+  <% if @parent_issue && @parent_issue.visible? %>
+    <%= text_field_tag( 'parent_issue', '', :value => @parent_issue) %>
+  <% else %>
+    <%= text_field_tag( 'parent_issue', '', :value => @parent_issue ? l(:text_private) : '') %>
+  <% end %>
+  <%= link_to_function( "Remove", 'clearValues(["issue_parent_id", "parent_issue"])') %>
+
+  <div id="parent_issue_candidates" class="autocomplete"></div>
+  <%= javascript_tag "observeParentIssueField('#{url_for(:controller => :issues,
+                                                         :action => :auto_complete_for_issue_parent,
+                                                         :id => @issue.id,
+                                                         :project_id => @project.id) }')" %>
+<% else %>
+  <%= @parent_issue || "-" %>
+<% end %>
+</p>
diff --git a/app/views/issues/_subissues_list.rhtml b/app/views/issues/_subissues_list.rhtml
new file mode 100644
index 0000000..b2c438e
--- /dev/null
+++ b/app/views/issues/_subissues_list.rhtml
@@ -0,0 +1,17 @@
+<% if @issue.root.self_and_descendants.size > 1 %>
+  <% content_for :header_tags do %>
+    <%= javascript_include_tag 'context_menu' %>
+    <%= stylesheet_link_tag 'context_menu' %>
+  <% end %>
+	<hr />
+  <p><strong><%=l(:label_issues_hierarchy)%></strong></p>
+  <div id="subissues">
+		<%= render( :partial => 'issues/list',
+								:locals => {
+									:issues 				 => @issue.root.self_and_descendants,
+									:emphasis_issues => [ @issue ],
+									:query  				 => @query }) %>
+  </div>
+  <div id="context-menu" style="display: none;"></div>
+  <%= javascript_tag "new ContextMenu('#{url_for(:controller => 'issues', :action => 'context_menu')}')" %>
+<% end %>
diff --git a/app/views/issues/context_menu.rhtml b/app/views/issues/context_menu.rhtml
index 4a1d0c3..3873dca 100644
--- a/app/views/issues/context_menu.rhtml
+++ b/app/views/issues/context_menu.rhtml
@@ -83,7 +83,7 @@
 		<ul>
 		<% (0..10).map{|x|x*10}.each do |p| -%>
 		    <li><%= context_menu_link "#{p}%", {:controller => 'issues', :action => 'bulk_edit', :ids => @issues.collect(&:id), 'done_ratio' => p, :back_to => @back}, :method => :post,
-		                                  :selected => (@issue && p == @issue.done_ratio), :disabled => !@can[:edit] %></li>
+		                                  :selected => (@issue && p == @issue.done_ratio), :disabled => (!@can[:edit] || !@issues.all? {|i| i.leaf? }) %></li>
 		<% end -%>
 		</ul>
 	</li>
diff --git a/app/views/issues/index.rhtml b/app/views/issues/index.rhtml
index 5b8fa05..dfb8328 100644
--- a/app/views/issues/index.rhtml
+++ b/app/views/issues/index.rhtml
@@ -29,6 +29,12 @@
 						<td><%= l(:field_group_by) %></td>
 						<td><%= select_tag('group_by', options_for_select([[]] + @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, @query.group_by)) %></td>
 					</tr>
+                    <% @query.view_options.each_key do |voption| -%>
+                    <tr>
+                      <td><%= @query.caption_for_view_option(voption) %></td>
+                      <td><%= select_tag( "view_options[#{voption}]", options_for_select(@query.values_for_view_option(voption), @query.view_options[voption])) %></td>
+                    </tr>
+                    <% end %>
 				</table>
 			</div>
 		</fieldset>
diff --git a/app/views/issues/show.rhtml b/app/views/issues/show.rhtml
index 914e557..7cc66a9 100644
--- a/app/views/issues/show.rhtml
+++ b/app/views/issues/show.rhtml
@@ -38,6 +38,10 @@
     <th class="estimated-hours"><%=l(:field_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.estimated_hours) %></td>
     <% end %>
 </tr>
+<% if !@issue.leaf? && @issue.original_estimated_hours %>
+   <td colspan="2">&nbsp;</td>
+   <th class="estimated-hours"><%=l(:field_original_estimated_hours)%>:</th><td class="estimated-hours"><%= l_hours(@issue.original_estimated_hours) %></td>
+<% end %>
 <%= render_custom_fields_rows(@issue) %>
 <%= call_hook(:view_issues_show_details_bottom, :issue => @issue) %>
 </table>
@@ -54,6 +58,8 @@
 
 <%= link_to_attachments @issue %>
 
+<%= render :partial => 'subissues_list' %>
+
 <%= call_hook(:view_issues_show_description_bottom, :issue => @issue) %>
 
 <% if authorize_for('issue_relations', 'new') || @issue.relations.any? %>
diff --git a/app/views/projects/roadmap.rhtml b/app/views/projects/roadmap.rhtml
index bcc3684..6e8821f 100644
--- a/app/views/projects/roadmap.rhtml
+++ b/app/views/projects/roadmap.rhtml
@@ -10,11 +10,13 @@
     <%= render(:partial => "wiki/content", :locals => {:content => version.wiki_page.content}) if version.wiki_page %>
 
     <% if (issues = @issues_by_version[version]) && issues.size > 0 %>
+    <% issues.each do |i|
+         issues += i.ancestors if i.child?
+       end
+       issues.uniq! %>
     <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
     <ul>
-    <%- issues.each do |issue| -%>
-        <li><%= link_to_issue(issue, :project => (@project != issue.project)) %></li>
-    <%- end -%>
+    <%= render_list_of_related_issues( issues, version) %>
     </ul>
     </fieldset>
     <% end %>
diff --git a/app/views/queries/_form.rhtml b/app/views/queries/_form.rhtml
index dcafe9f..c70fb5f 100644
--- a/app/views/queries/_form.rhtml
+++ b/app/views/queries/_form.rhtml
@@ -22,6 +22,14 @@
 
 <p><label for="query_group_by"><%= l(:field_group_by) %></label>
 <%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
+
+<% @query.view_options.each_key do |voption| -%>
+<p><label><%= @query.caption_for_view_option(voption) %></label>
+<%= select_tag("view_options[#{voption}]", options_for_select(@query.values_for_view_option(voption), @query.view_options[voption])) %></p>
+<% end %>
+
+<p><label for="query_group_by"><%= l(:field_group_by) %></label>
+<%= select 'query', 'group_by', @query.groupable_columns.collect {|c| [c.caption, c.name.to_s]}, :include_blank => true %></p>
 </div>
 
 <fieldset><legend><%= l(:label_filter_plural) %></legend>
diff --git a/app/views/settings/_issues.rhtml b/app/views/settings/_issues.rhtml
index 4280e44..eb86b87 100644
--- a/app/views/settings/_issues.rhtml
+++ b/app/views/settings/_issues.rhtml
@@ -8,6 +8,10 @@
 <p><%= setting_select :issue_done_ratio, Issue::DONE_RATIO_OPTIONS.collect {|i| [l("setting_issue_done_ratio_#{i}"), i]} %></p>
 
 <p><%= setting_text_field :issues_export_limit, :size => 6 %></p>
+
+<p><%= setting_select :delete_children, [ [l(:general_text_Yes), "1" ], [l(:general_text_No),  "0" ] ] %></p>
+
+<p><%= setting_select :reopened_parent_issue_status, [["", 0]] + IssueStatus.all(:order => 'position ASC').collect{|status| [status.name, status.id.to_s]} %></p>
 </div>
 
 <fieldset class="box settings"><legend><%= l(:setting_issue_list_default_columns) %></legend>
@@ -15,5 +19,10 @@
         Query.new.available_columns.collect {|c| [c.caption, c.name.to_s]}, :label => false) %>
 </fieldset>
 
+
+<fieldset class="box settings"><legend><%= l(:setting_subissues_list_columns) %></legend>
+<%= setting_multiselect(:subissues_list_columns, Query.new.available_columns.collect {|c| [c.caption, c.name.to_s]}, :label => false) %>
+</fieldset>
+
 <%= submit_tag l(:button_save) %>
 <% end %>
diff --git a/app/views/versions/show.rhtml b/app/views/versions/show.rhtml
index 18bc6bc..6b372ed 100644
--- a/app/views/versions/show.rhtml
+++ b/app/views/versions/show.rhtml
@@ -33,15 +33,17 @@
 <%= render(:partial => "wiki/content", :locals => {:content => @version.wiki_page.content}) if @version.wiki_page %>
 
 <% issues = @version.fixed_issues.find(:all,
-                                       :include => [:status, :tracker, :priority],
-                                       :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id") %>
+                                       :include => [:status, :tracker, :priority, :fixed_version],
+                                       :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
+       issues ||= []
+       issues.each do |i|
+         issues += i.ancestors if i.child?
+       end
+       issues.uniq!
+%>
 <% if issues.size > 0 %>
 <fieldset class="related-issues"><legend><%= l(:label_related_issues) %></legend>
-<ul>
-<% issues.each do |issue| -%>
-    <li><%= link_to_issue(issue) %></li>
-<% end -%>
-</ul>
+  <%= render_list_of_related_issues( issues, @version) %>
 </fieldset>
 <% end %>
 </div>
diff --git a/config/environment.rb b/config/environment.rb
index 97fc54a..8958080 100644
--- a/config/environment.rb
+++ b/config/environment.rb
@@ -51,7 +51,7 @@ Rails::Initializer.run do |config|
   config.action_mailer.perform_deliveries = false
 
   config.gem 'rubytree', :lib => 'tree'
-  
+  config.action_controller.session = { :key => "_myapp_session", :secret => "cvxcnmbvmnsdfhjkhw3kjhwkbmnwefbmnsdbfsdkjfsDFSDF:SDFjwh3kj23jk42342" }
   # Load any local configuration that is kept out of source control
   # (e.g. gems, patches).
   if File.exists?(File.join(File.dirname(__FILE__), 'additional_environment.rb'))
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 41c5f2a..aeab428 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -162,6 +162,8 @@ en:
   error_issue_done_ratios_not_updated: "Issue done ratios not updated."
   error_workflow_copy_source: 'Please select a source tracker or role'
   error_workflow_copy_target: 'Please select target tracker(s) and role(s)'
+  error_issue_subtasks_cant_close_parent: "Can't close parent issue while one of the children is still open."
+  error_issue_subtasks_cant_select_lower_target_version: "Can't set target version of parent issue lower than any of the children."
   
   warning_attachments_not_saved: "{{count}} file(s) could not be saved."
   
@@ -261,6 +263,7 @@ en:
   field_assignable: Issues can be assigned to this role
   field_redirect_existing_links: Redirect existing links
   field_estimated_hours: Estimated time
+  field_original_estimated_hours: Original estimated time
   field_column_names: Columns
   field_time_zone: Time zone
   field_searchable: Searchable
@@ -273,6 +276,7 @@ en:
   field_content: Content
   field_group_by: Group results by
   field_sharing: Sharing
+  field_parent_issue: Child of
   
   setting_app_title: Application title
   setting_app_subtitle: Application subtitle
@@ -326,6 +330,9 @@ en:
   setting_issue_done_ratio_issue_status: Use the issue status
   setting_start_of_week: Start calendars on
   setting_rest_api_enabled: Enable REST web service
+  setting_subissues_list_columns: Columns for subissues list
+  setting_delete_children: Delete children when parent destroyed
+  setting_reopened_parent_issue_status: Status applied to parent when reopening
   
   permission_add_project: Create project
   permission_add_subprojects: Create subprojects
@@ -740,6 +747,11 @@ en:
   label_api_access_key: API access key
   label_missing_api_access_key: Missing an API access key
   label_api_access_key_created_on: "API access key created {{value}} ago"
+  label_view_option_parents_do_not_show: Never
+  label_view_option_parents_show_always: Always
+  label_view_option_parents_show_and_group: Organize by parent
+  label_issues_hierarchy: Issues hierarchy
+  label_view_option_show_parents: Show parents
   
   button_login: Login
   button_submit: Submit
@@ -784,6 +796,7 @@ en:
   button_quote: Quote
   button_duplicate: Duplicate
   button_show: Show
+  button_add_subissue: Add sub-issue
   
   status_active: active
   status_registered: registered
@@ -850,6 +863,7 @@ en:
   text_wiki_page_destroy_children: "Delete child pages and all their descendants"
   text_wiki_page_reassign_children: "Reassign child pages to this parent page"
   text_own_membership_delete_confirmation: "You are about to remove some or all of your permissions and may no longer be able to edit this project after that.\nAre you sure you want to continue?"
+  text_private: Private
   
   default_role_manager: Manager
   default_role_developper: Developer
diff --git a/config/settings.yml b/config/settings.yml
index cebfbb5..cf0764d 100644
--- a/config/settings.yml
+++ b/config/settings.yml
@@ -180,3 +180,15 @@ start_of_week:
   default: ''
 rest_api_enabled:
   default: 0
+delete_children:
+  default: 1
+subissues_list_columns:
+  serialized: true
+  default:
+  - id
+  - subject
+  - status
+  - start_date
+  - due_date
+reopened_parent_issue_status:
+  default: ''
diff --git a/db/migrate/20090115162651_add_queries_view_options.rb b/db/migrate/20090115162651_add_queries_view_options.rb
new file mode 100644
index 0000000..cfd0377
--- /dev/null
+++ b/db/migrate/20090115162651_add_queries_view_options.rb
@@ -0,0 +1,9 @@
+class AddQueriesViewOptions < ActiveRecord::Migration
+  def self.up
+    add_column :queries, :view_options, :text
+  end
+
+  def self.down
+    remove_column :queries, :view_options
+  end
+end
diff --git a/db/migrate/20090115162652_add_default_value_of_view_optoins_queries.rb b/db/migrate/20090115162652_add_default_value_of_view_optoins_queries.rb
new file mode 100644
index 0000000..450f246
--- /dev/null
+++ b/db/migrate/20090115162652_add_default_value_of_view_optoins_queries.rb
@@ -0,0 +1,11 @@
+class AddDefaultValueOfViewOptoinsQueries < ActiveRecord::Migration
+  def self.up
+    Query.find(:all).each do |q|
+      q.view_options ||= { 'show_parents' => 'do_not_show' }
+      q.save!
+    end
+  end
+
+  def self.down
+  end
+end
diff --git a/db/migrate/20090406213813_add_issues_parent_id_lft_and_rgt.rb b/db/migrate/20090406213813_add_issues_parent_id_lft_and_rgt.rb
new file mode 100644
index 0000000..8a0ecf0
--- /dev/null
+++ b/db/migrate/20090406213813_add_issues_parent_id_lft_and_rgt.rb
@@ -0,0 +1,14 @@
+class AddIssuesParentIdLftAndRgt < ActiveRecord::Migration
+
+  def self.up
+    add_column :issues, :parent_id, :integer, :default => nil
+    add_column :issues, :lft, :integer
+    add_column :issues, :rgt, :integer
+  end
+
+  def self.down
+    remove_column :issues, :parent_id
+    remove_column :issues, :lft
+    remove_column :issues, :rgt
+  end
+end
diff --git a/db/migrate/20090406213899_issues_rebuild.rb b/db/migrate/20090406213899_issues_rebuild.rb
new file mode 100644
index 0000000..0cad2b4
--- /dev/null
+++ b/db/migrate/20090406213899_issues_rebuild.rb
@@ -0,0 +1,15 @@
+# Need to assume Issues are valid in order to rebuild.
+class Issue < ActiveRecord::Base
+  def valid?
+    true
+  end
+end
+
+class IssuesRebuild < ActiveRecord::Migration
+  def self.up
+    Issue.rebuild!
+  end
+
+  def self.down
+  end
+end
diff --git a/db/migrate/20091211204929_add_lft_rgt_indexes_to_issues.rb b/db/migrate/20091211204929_add_lft_rgt_indexes_to_issues.rb
new file mode 100644
index 0000000..98e0dca
--- /dev/null
+++ b/db/migrate/20091211204929_add_lft_rgt_indexes_to_issues.rb
@@ -0,0 +1,11 @@
+class AddLftRgtIndexesToIssues < ActiveRecord::Migration
+  def self.up
+    add_index :issues, :lft
+    add_index :issues, :rgt
+  end
+
+  def self.down
+    remove_index :issues, :lft
+    remove_index :issues, :rgt
+  end
+end
diff --git a/db/migrate/20091211205222_add_indexes_to_issues_parent_id.rb b/db/migrate/20091211205222_add_indexes_to_issues_parent_id.rb
new file mode 100644
index 0000000..e1edb63
--- /dev/null
+++ b/db/migrate/20091211205222_add_indexes_to_issues_parent_id.rb
@@ -0,0 +1,9 @@
+class AddIndexesToIssuesParentId < ActiveRecord::Migration
+  def self.up
+    add_index :issues, :parent_id
+  end
+
+  def self.down
+    remove_index :issues, :parent_id
+  end
+end
diff --git a/lib/redmine.rb b/lib/redmine.rb
index 0cf0cc4..f3c08d8 100644
--- a/lib/redmine.rb
+++ b/lib/redmine.rb
@@ -45,7 +45,7 @@ Redmine::AccessControl.map do |map|
                                   :reports => :issue_report}
     map.permission :add_issues, {:issues => [:new, :update_form]}
     map.permission :edit_issues, {:issues => [:edit, :reply, :bulk_edit, :update_form]}
-    map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy]}
+    map.permission :manage_issue_relations, {:issue_relations => [:new, :destroy], :issues => [:add_subissue, :auto_complete_for_issue_parent]}
     map.permission :add_issue_notes, {:issues => [:edit, :reply]}
     map.permission :edit_issue_notes, {:journals => :edit}, :require => :loggedin
     map.permission :edit_own_issue_notes, {:journals => :edit}, :require => :loggedin
diff --git a/public/images/contract.png b/public/images/contract.png
new file mode 100644
index 0000000000000000000000000000000000000000..69566aeb07e3a90b815d07f667310cb4273d4600
GIT binary patch
literal 290
zcmeAS@N?(olHy`uVBq!ia0vp^oFL4>1|%O$WD@{VEX7WqAsj$Z!;#Vfk}U9uEC#B-
z4#JF18nY{af|4b!5hcO-X(i=}MX3zs<>h*rdD+Fui3O>8`9<lo-`Pk370vf_aSV}|
zTHEiKcPM~|_4!S|mQ!B?_%ARQvVL=k;a@oa$$mY%j!iG$==9oboA&zV7v*m*XBKU>
zmSj&6PTZjRYvSea{5uqQN(zi_?9dfIG_OtJ0F!#%t^fv?Gfgl4xH1WC`zH~UU7n!6
zDWL18c0kvvMMWvvj~V7g+;)3befwUeDd$awC3aoOWj6U-%+f4x;vB74^t*5AlQG-=
d^icgDrt6Pvb-&vSE(SW4!PC{xWt~$(69CxbYH|Po

literal 0
HcmV?d00001

diff --git a/public/images/corner-dots.gif b/public/images/corner-dots.gif
new file mode 100644
index 0000000000000000000000000000000000000000..470d7109951c83f0a024cd8e96bda02c93d96454
GIT binary patch
literal 59
zcmZ?wbhEHb6ky<CXkcLY|NlP&1B2pE7Dgb&paUX6G7L;YE%}Y7-!7Opr!XpPeOt^^
J-YixIYXGYY5D@?X

literal 0
HcmV?d00001

diff --git a/public/images/expand.png b/public/images/expand.png
new file mode 100644
index 0000000000000000000000000000000000000000..ca4f30f5f9493fc75cf37ebc5e22049f4cfc9f31
GIT binary patch
literal 2939
zcmV->3xxEEP)<h;3K|Lk000e1NJLTq000R9000RH1^@s6;E@Ip00009a7bBm000fw
z000fw0YWI7cmMzjPiaF#P*7-ZbZ>KLZ*U+<Lqi~Na&Km7Y-Iodc-oy)XH-+^7Crag
z^g>IBfRsybQWXdwQbLP>6p<z>Aqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uh<iVD~V
z<RPMtgQJLw%KPDaqifc@_vX$1wbwr9tn;0-&j-K=43<bUQ8j=JsX`tR;Dg7+#^K~H
zK!FM*Z~zbpvt%K2{UZSY_<lS*D<Z%Lz5oGu(+dayz)hRLFdT>f59&ghTmgWD0l;*T
zI7<kC6aYYajzXpYKt=(8otP$50H6c_V9R4-;{Z@C0AMG7=F<Rxo%or10RUT+Ar%3j
zkpLhQWr#!oXgdI`&sK^>09Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p
z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-<?i
z0%4j!F2Z@488U%158(66005wo6%pWr^Zj_v4zAA5HjcIqUoGmt2LB>rV&neh&#Q1i
z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_<lS*MWK+n+1cgf
z<k(8YLR(?VSAG6x!e78w{cQPuJpA|d;J)G{fihizM+Erb!p!tcr5w+a34~(Y=8s4G
zw+sLL9n&JjNn*KJDiq^U5^;`1nvC-@r6P$!k}1U{(*I=Q-z@tBKHoI}uxdU5dyy@u
zU1J0GOD7Ombim^G008p4Z^6_k2m^p<gW=D2|L;HjN1!DDfM!XOaR2~bL?kX$%CkSm
z2mk;?pn)o|K^yeJ7%adB9Ki+L!3+FgHiSYX#KJ-lLJDMn9CBbOtb#%)hRv`YDqt_v
zKpix|QD}yfa1JiQRk#j4a1Z)n2%f<xynzV>LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW
zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_Ifq<Ex{*7`05XF7hP+2Hl!3BQJ=6@fL%FCo
z8iYoo3(#bAF`ADSpqtQgv>H8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X
zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ<AYmRsNLWl*PS{AOARHt#5!wki2?K;t
z!Y3k=s7tgax)J%r7-BLphge7~Bi0g+6E6^Zh(p9TBoc{3GAFr^0!gu?RMHaCM$&Fl
zBk3%un>0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4
z<uv66WtcKSRim0x-Ke2d5jBrmLam{;Qm;{ms1r1GnmNsb7D-E`t)i9F8fX`2_i3-_
zbh;7Ul^#x)&{xvS=|||7=mYe33=M`AgU5(xC>fg=2N-7=cNnjjOr{yriy6mMFgG#l
znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U
zt5vF<Q0r40Q)j6=sE4X&sBct1q<&fbi3VB2Ov6t@q*0);U*o*SAPZv|vv@2aYYnT0
zb%8a+Cb7-ge0D0knEf5Qi#@8Tp*ce{N;6lpQuCB%KL_KOarm5cP6_8Ir<e17iry6O
zDdH&`rZh~sF=bq9s+O0QSgS~@QL9Jmy*94xr=6y~MY~!1fet~(N+(<=M`w@D1)b+p
z*;C!83a1uLJv#NSE~;y#8=<>IcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya?
z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y
zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB
zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt
z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a<fJbF^|4I#xQ~n$Dc=
zKYhjYmgz5NSkDm8*fZm{6U!;YX`NG>(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C
z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB
zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe
zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0
z?2xS?_ve_-k<Mujg;0Lz*3buG=3$G&ehepthlN*$KaOySSQ^nWmo<0M+(UEUMEXRQ
zMBbZcF;6+KElM>iKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$
z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4
z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu
zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu
z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E
ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw
zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX
z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i&
z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01
z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R
z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw
zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD
zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3|
zawq-H%e&ckC+@AhPrP6BK<z=<L*0kfKU@CX*zeqbYQT4(^U>T#_XdT7&;F71j}Joy
zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z
zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot<a{81DF0~rvGr5Xr~8u`lav1h
z1DNytV>2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F}
z0001{Nkl<Zc-n-IK~4if3<RrfZxSirLp*>7@c=G4L*RQ6%+5@6m_!_@o28Pft?S3D
znP=>cIEdD6WSo#qgu$kmXP%y4zy)Dof|Et(>v|#VZ3zM_B98d(LIDtQ5X9tx8Aw1v
zpWjPr?FI}Y3#HhOLMf_>%9X8*6L@29olH;)4>Kl@CLtSu)XHV#-|e_>3IiPihIe}Z
l0nPP@b^!GG{$~E+cK}YdLLMYnRc-(P002ovPDHLkV1k-6a(@5-

literal 0
HcmV?d00001

diff --git a/public/javascripts/application.js b/public/javascripts/application.js
index 57419d0..fc27862 100644
--- a/public/javascripts/application.js
+++ b/public/javascripts/application.js
@@ -52,7 +52,7 @@ function addFileField() {
     d.type = "text";
     d.name = "attachments[" + fileFieldCount + "][description]";
     d.size = 60;
-    
+
     p = document.getElementById("attachments_fields");
     p.appendChild(document.createElement("br"));
     p.appendChild(f);
@@ -194,6 +194,25 @@ function randomKey(size) {
 	return key;
 }
 
+function clearValues(dom_ids) {
+  dom_ids.each(function(dom_id) {
+    $(dom_id).value = '';
+  });
+}
+
+function observeParentIssueField(url) {
+  new Ajax.Autocompleter('parent_issue',
+                         'parent_issue_candidates',
+                         url,
+                         {
+                           minChars: 1,
+                           frequency: 0.5,
+                           paramName: 'issue_parent',
+                           afterUpdateElement: function(element, value) {
+                             document.getElementById('issue_parent_id').value = value.id;
+                           }});
+}
+
 /* shows and hides ajax indicator */
 Ajax.Responders.register({
     onCreate: function(){
diff --git a/public/stylesheets/application.css b/public/stylesheets/application.css
index 64741d7..eec2cff 100644
--- a/public/stylesheets/application.css
+++ b/public/stylesheets/application.css
@@ -602,6 +602,7 @@ div.autocomplete ul li {
   border-bottom: 1px solid #ccc;
   border-left: 1px solid #ccc;
   border-right: 1px solid #ccc;
+  background-color: white;
 }
 div.autocomplete ul li span.informal {
   font-size: 80%;
@@ -846,3 +847,43 @@ h2 img { vertical-align:middle; }
   #content { width: 99%; margin: 0; padding: 0; border: 0; background: #fff; overflow: visible !important;}
 	#wiki_add_attachment { display:none; }
 }
+
+/***** Subtasks *****/
+.issue-subject-in-tree, .issue-subject-in-tree .issue-subject {
+    background-image: url(../images/corner-dots.gif);
+    background-repeat: no-repeat;
+    background-position: -3px center;
+}
+
+/* Used to show issues which is not pass by filter, but should by
+   shown as parent for other issue. */
+.issue-unfiltered {
+    opacity: 0.5;
+    filter: alpha(opacity=50);
+}
+
+.issue-emphasis {
+  font-weight: bold;    
+}
+
+.expanded-issue {
+	background-image: url(contract.png);
+	background-repeat: no-repeat;
+}
+
+.contracted-issue {
+	background-image: url(expand.png);
+	background-repeat: no-repeat;
+}
+
+.expand-icon, .contract-icon{
+/* 	position: absolute; */
+	vertical-align: middle;
+}
+
+/*text after the icon, which needs to be indented so that all its lines stay completely after the icon*/
+.issue-subject{
+	padding-left: 1em;
+}
+
+input#parent_issue {width: 80%;}
\ No newline at end of file
diff --git a/test/fixtures/issues.yml b/test/fixtures/issues.yml
index 4b61b41..91b1003 100644
--- a/test/fixtures/issues.yml
+++ b/test/fixtures/issues.yml
@@ -15,6 +15,8 @@ issues_001:
   status_id: 1
   start_date: <%= 1.day.ago.to_date.to_s(:db) %>
   due_date: <%= 10.day.from_now.to_date.to_s(:db) %>
+  lft: '1'
+  rgt: '2'
 issues_002: 
   created_on: 2006-07-19 21:04:21 +02:00
   project_id: 1
@@ -31,6 +33,8 @@ issues_002:
   status_id: 2
   start_date: <%= 2.day.ago.to_date.to_s(:db) %>
   due_date: 
+  lft: '3'
+  rgt: '4'
 issues_003: 
   created_on: 2006-07-19 21:07:27 +02:00
   project_id: 1
@@ -47,6 +51,8 @@ issues_003:
   status_id: 1
   start_date: <%= 1.day.from_now.to_date.to_s(:db) %>
   due_date: <%= 40.day.ago.to_date.to_s(:db) %>
+  lft: '5'
+  rgt: '6'
 issues_004: 
   created_on: <%= 5.days.ago.to_date.to_s(:db) %>
   project_id: 2
@@ -61,6 +67,8 @@ issues_004:
   assigned_to_id: 2
   author_id: 2
   status_id: 1
+  lft: '7'
+  rgt: '8'
 issues_005: 
   created_on: <%= 5.days.ago.to_date.to_s(:db) %>
   project_id: 3
@@ -75,6 +83,8 @@ issues_005:
   assigned_to_id: 
   author_id: 2
   status_id: 1
+  lft: '9'
+  rgt: '10'
 issues_006: 
   created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
   project_id: 5
@@ -91,6 +101,8 @@ issues_006:
   status_id: 1
   start_date: <%= Date.today.to_s(:db) %>
   due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
+  lft: '11'
+  rgt: '12'
 issues_007: 
   created_on: <%= 10.days.ago.to_date.to_s(:db) %>
   project_id: 1
@@ -108,6 +120,8 @@ issues_007:
   start_date: <%= 10.days.ago.to_s(:db) %>
   due_date: <%= Date.today.to_s(:db) %>
   lock_version: 0
+  lft: '13'
+  rgt: '14'
 issues_008: 
   created_on: <%= 10.days.ago.to_date.to_s(:db) %>
   project_id: 1
@@ -125,6 +139,8 @@ issues_008:
   start_date: 
   due_date: 
   lock_version: 0
+  lft: '15'
+  rgt: '16'
 issues_009: 
   created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
   project_id: 5
@@ -141,6 +157,8 @@ issues_009:
   status_id: 1
   start_date: <%= Date.today.to_s(:db) %>
   due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
+  lft: '17'
+  rgt: '18'
 issues_010: 
   created_on: <%= 1.minute.ago.to_date.to_s(:db) %>
   project_id: 5
@@ -157,6 +175,8 @@ issues_010:
   status_id: 1
   start_date: <%= Date.today.to_s(:db) %>
   due_date: <%= 1.days.from_now.to_date.to_s(:db) %>
+  lft: '19'
+  rgt: '20'
 issues_011: 
   created_on: <%= 3.days.ago.to_date.to_s(:db) %>
   project_id: 1
@@ -173,6 +193,8 @@ issues_011:
   status_id: 5
   start_date: <%= 1.day.ago.to_date.to_s(:db) %>
   due_date:
+  lft: '21'
+  rgt: '22'
 issues_012: 
   created_on: <%= 3.days.ago.to_date.to_s(:db) %>
   project_id: 1
@@ -189,6 +211,8 @@ issues_012:
   status_id: 5
   start_date: <%= 1.day.ago.to_date.to_s(:db) %>
   due_date:
+  lft: '23'
+  rgt: '24'
 issues_013:
   created_on: <%= 5.days.ago.to_date.to_s(:db) %>
   project_id: 3
@@ -203,3 +227,148 @@ issues_013:
   assigned_to_id: 
   author_id: 2
   status_id: 1
+  lft: '25'
+  rgt: '26'
+issues_root:
+  created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  project_id: 1
+  updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  priority_id: 4
+  subject: root
+  id: 14
+  fixed_version_id: 
+  category_id: 
+  description: This is root of subissues.
+  tracker_id: 1
+  assigned_to_id: 
+  author_id: 2
+  status_id: 1
+  start_date: 
+  due_date: 
+  lock_version: 0
+  lft: "27"
+  rgt: "38"
+  parent_id:
+
+issues_child001:
+  created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  project_id: 1
+  updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  priority_id: 4
+  subject: child001
+  id: 15
+  fixed_version_id: 
+  category_id: 
+  description: This is child001 of root.
+  tracker_id: 1
+  assigned_to_id: 
+  author_id: 2
+  status_id: 1
+  start_date: 
+  due_date: 
+  lock_version: 0
+  parent_id: 14
+  lft: "28"
+  rgt: "33"
+issues_child002:
+  created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  project_id: 1
+  updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  priority_id: 4
+  subject: child002
+  id: 16
+  fixed_version_id: 
+  category_id: 
+  description: This is child002 of root.
+  tracker_id: 1
+  assigned_to_id: 
+  author_id: 2
+  status_id: 1
+  start_date: 
+  due_date: 
+  lock_version: 0
+  parent_id: 14
+  lft: "34"
+  rgt: "37"
+issues_subchild001:
+  created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  project_id: 1
+  updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  priority_id: 4
+  subject: subchild001
+  id: 17
+  fixed_version_id: 
+  category_id: 
+  description: This is subchild001 of child001.
+  tracker_id: 1
+  assigned_to_id: 
+  author_id: 2
+  status_id: 1
+  start_date: 
+  due_date: 
+  lock_version: 0
+  parent_id: 15
+  lft: "29"
+  rgt: "30"
+issues_subchild002:
+  created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  project_id: 1
+  updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  priority_id: 4
+  subject: subchild002
+  id: 18
+  fixed_version_id: 
+  category_id: 
+  description: This is subchild002 of child001.
+  tracker_id: 1
+  assigned_to_id: 
+  author_id: 2
+  status_id: 1
+  start_date: 
+  due_date: 
+  lock_version: 0
+  parent_id: 15
+  lft: "31"
+  rgt: "32"
+issues_subchild003:
+  created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  project_id: 1
+  updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  priority_id: 4
+  subject: subchild003
+  id: 19
+  fixed_version_id: 
+  category_id: 
+  description: This is subchild003 of child002.
+  tracker_id: 1
+  assigned_to_id: 
+  author_id: 2
+  status_id: 1
+  start_date: 
+  due_date:  <%= 10.days.from_now.to_date.to_s(:db) %>
+  lock_version: 0
+  parent_id: 16
+  lft: "35"
+  rgt: "36"
+
+issue_leaf_from_another_project:
+  created_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  project_id: 2
+  updated_on: <%= 10.days.ago.to_date.to_s(:db) %>
+  priority_id: 4
+  subject: issue_leaf_from_another_project
+  id: 20
+  fixed_version_id: 4
+  category_id: 3
+  description: This is issue from another project.
+  tracker_id: 1
+  assigned_to_id: 
+  author_id: 2
+  status_id: 1
+  start_date: 
+  due_date: 
+  lock_version: 0
+  parent_id: nil
+  lft: "13"
+  rgt: "14"
+
diff --git a/test/fixtures/queries.yml b/test/fixtures/queries.yml
index a49f82f..b5ec976 100644
--- a/test/fixtures/queries.yml
+++ b/test/fixtures/queries.yml
@@ -21,6 +21,9 @@ queries_001:
 
   user_id: 1
   column_names: 
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 queries_002: 
   id: 2
   project_id: 1
@@ -39,6 +42,9 @@ queries_002:
 
   user_id: 3
   column_names: 
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 queries_003: 
   id: 3
   project_id: 
@@ -53,6 +59,9 @@ queries_003:
 
   user_id: 3
   column_names: 
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 queries_004: 
   id: 4
   project_id: 
@@ -67,6 +76,9 @@ queries_004:
 
   user_id: 2
   column_names: 
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 queries_005: 
   id: 5
   project_id: 
@@ -87,6 +99,9 @@ queries_005:
       - desc
     - - tracker
       - asc
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 queries_006: 
   id: 6
   project_id: 
@@ -106,6 +121,9 @@ queries_006:
     --- 
     - - priority
       - desc
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 queries_007: 
   id: 7
   project_id: 2
@@ -120,6 +138,9 @@ queries_007:
 
   user_id: 2
   column_names: 
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 queries_008: 
   id: 8
   project_id: 2
@@ -134,6 +155,9 @@ queries_008:
 
   user_id: 2
   column_names: 
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 queries_009: 
   id: 9
   project_id: 
@@ -153,4 +177,7 @@ queries_009:
     --- 
     - - priority
       - desc
+  view_options: |
+    ---
+    'show_parents': 'do_not_show'
 
diff --git a/test/fixtures/versions.yml b/test/fixtures/versions.yml
index 3b59a2f..7e840a5 100644
--- a/test/fixtures/versions.yml
+++ b/test/fixtures/versions.yml
@@ -69,3 +69,22 @@ versions_007:
   effective_date: 
   status: open
   sharing: 'system'
+onlinestore_1_0: 
+  created_on: 2006-07-19 21:00:33 +02:00
+  name: "1.0"
+  project_id: 2
+  updated_on: 2006-07-19 21:00:33 +02:00
+  id: 8
+  description: Future version
+  effective_date: 
+  status: open
+versions_009: 
+  created_on: 2006-07-19 21:00:33 +02:00
+  name: "2.1"
+  project_id: 1
+  updated_on: 2006-07-19 21:00:33 +02:00
+  id: 9
+  description: Future version
+  effective_date: 
+  status: open
+
diff --git a/test/functional/issues_controller_test.rb b/test/functional/issues_controller_test.rb
index e73e24a..01daa60 100644
--- a/test/functional/issues_controller_test.rb
+++ b/test/functional/issues_controller_test.rb
@@ -267,6 +267,15 @@ class IssuesControllerTest < ActionController::TestCase
   end
 
   def test_gantt
+    parent_issue = Issue.find(14)
+    parent_issue.update_attributes(:start_date => 1.day.ago.to_date)
+    parent_issue.reload
+    assert_not_nil parent_issue.due_date
+    assert_nil parent_issue.read_attribute(:due_date)
+
+    subissue = Issue.generate_for_project!(Project.find(1), :start_date => 1.day.ago.to_date, :due_date => 10.days.from_now.to_date)
+    subissue.move_to_child_of Issue.find(14)
+    
     get :gantt, :project_id => 1
     assert_response :success
     assert_template 'gantt.rhtml'
@@ -274,9 +283,12 @@ class IssuesControllerTest < ActionController::TestCase
     events = assigns(:gantt).events
     assert_not_nil events
     # Issue with start and due dates
+    assert events.include?(subissue)
     i = Issue.find(1)
     assert_not_nil i.due_date
     assert events.include?(Issue.find(1))
+    # Parent issue with a child with a start and due date
+    assert events.include?(Issue.find(14))
     # Issue with without due date but targeted to a version with date
     i = Issue.find(2)
     assert_nil i.due_date
@@ -492,7 +504,38 @@ class IssuesControllerTest < ActionController::TestCase
     assert_tag :tag => 'div', :attributes => { :class => /error/ },
                               :content => /No tracker/
   end
-  
+
+  context "GET to :new" do
+    context "with a parent_id" do
+      setup do
+        @request.session[:user_id] = 3
+      end
+      
+      should "set the parent issue" do
+        get :new, :project_id => 1, :issue => {:parent_id => 1}
+        assert_response :success
+        assert_template 'new'
+        assert_equal Issue.find(1), assigns(:parent_issue)
+      end
+
+      should "not set the parent issue if the parameter points to a missing issue" do
+        get :new, :project_id => 1, :issue => {:parent_id => 1_000_000}
+        assert_response :success
+        assert_template 'new'
+        assert_equal nil, assigns(:parent_issue)
+      end
+
+      should "not set the parent issue if the parameter points to an unauthorized issue" do
+        issue = Issue.generate_for_project!(Project.find(5))
+        get :new, :project_id => 1, :issue => {:parent_id => issue.id}
+        assert_response :success
+        assert_template 'new'
+        assert_equal nil, assigns(:parent_issue)
+      end
+    end
+      
+  end
+
   def test_update_new_form
     @request.session[:user_id] = 2
     xhr :post, :update_form, :project_id => 1,
@@ -924,6 +967,20 @@ class IssuesControllerTest < ActionController::TestCase
                           :content => notes
     assert_tag :input, :attributes => { :name => 'time_entry[hours]', :value => "2z" }
   end
+
+  def test_post_edit_with_parent_id_set_to_self
+    issue = Issue.find(1)
+    assert_equal nil, issue.parent
+    @request.session[:user_id] = 2
+
+    post :edit,
+         :id => 1,
+         :issue => { :parent_id => 1}
+
+    assert_redirected_to :action => 'show', :id => '1'
+    issue.reload
+    assert_equal nil, issue.parent
+  end
   
   def test_post_edit_should_allow_fixed_version_to_be_set_to_a_subproject
     issue = Issue.find(2)
@@ -1171,6 +1228,18 @@ class IssuesControllerTest < ActionController::TestCase
                                              :class => 'icon-del' }
   end
 
+  test 'context_menu with a parent issue' do
+    @request.session[:user_id] = 2
+    @issue = Issue.generate_for_project!(Project.find(1), :subject => 'test')
+    @issue.move_to_child_of Issue.find(1)
+
+    get :context_menu, :ids => [1]
+
+    assert_response :success
+    assert_template 'context_menu'
+    assert_select 'a[class*=disabled]', :text => /0%/
+  end
+
   def test_context_menu_one_issue_by_anonymous
     get :context_menu, :ids => [1]
     assert_response :success
@@ -1205,6 +1274,18 @@ class IssuesControllerTest < ActionController::TestCase
                                              :class => 'icon-del' }
   end
 
+  test 'context_menu with multiple issues and a parent issue' do
+    @request.session[:user_id] = 2
+    @issue = Issue.generate_for_project!(Project.find(1), :subject => 'test')
+    @issue.move_to_child_of Issue.find(1)
+
+    get :context_menu, :ids => [1,2]
+
+    assert_response :success
+    assert_template 'context_menu'
+    assert_select 'a[class*=disabled]', :text => /0%/
+  end
+  
   def test_context_menu_multiple_issues_of_different_project
     @request.session[:user_id] = 2
     get :context_menu, :ids => [1, 2, 4]
@@ -1271,4 +1352,267 @@ class IssuesControllerTest < ActionController::TestCase
                      :child => {:tag => 'form',
                                 :child => {:tag => 'input', :attributes => {:name => 'issues', :type => 'hidden', :value => '1'}}}
   end
+
+  def test_new_child_issue
+    child_issue_subject = "This is the test_new child issue"
+    parent_issue = issues( :issues_root)
+    @request.session[:user_id] = 2
+
+    post( :new, :project_id => 1,
+          :parent_issue => parent_issue,
+          :issue => {:tracker_id => 3,
+            :subject => child_issue_subject,
+            :description => 'This is the description',
+            :priority_id => 5,
+            :parent_id => parent_issue.id.to_s,
+            :estimated_hours => '',
+            :custom_field_values => {'2' => 'Value for field 2'}})
+    child = Issue.find_by_subject( child_issue_subject)
+
+    assert_redirected_to "issues/#{child.id}"
+    assert( child.parent == parent_issue,
+            "New child has Issue id=#{child.parent} as parent, not id=#{parent_issue}")
+  end
+
+  def test_edit_issue_set_parent
+    parent_issue = issues( :issues_root)
+    moving_issue = issues( :issues_subchild003)
+    @request.session[:user_id] = 2
+
+    post( :edit,
+          :id => moving_issue.id,
+          :project_id => 1,
+          :parent_issue => parent_issue,
+          :issue => {
+            :parent_id => parent_issue.id
+          })
+    assert_redirected_to :action => 'show', :id => moving_issue.id
+    assert moving_issue.reload.parent == parent_issue
+  end
+
+  def test_move_child_to_root
+    parent = issues( :issues_root)
+    child = issues( :issues_child001)
+
+    post( :edit,
+          :id => child.id,
+          :project_id => 1,
+          :issue => {
+            :parent_id => "",
+          })
+    assert_redirected_to :controller => 'issues', :action => 'show', :id => child.id
+    assert child.reload.root?
+  end
+
+  def test_add_subissue_should_redirect_to_action_new
+    @request.session[:user_id] = 2
+    get( :add_subissue, :project_id => 1,
+         :issue => {
+           :tracker_id      => 3,
+           :priority_id     => 5,
+           :subject         => "test_add_subissue",
+           :description     => "test_add_subissue",
+           :estimated_hours => '' },
+         :parent_issue_id => 1)
+    assert_redirected_to :controller => 'issues', :action => "new", :project_id => Project.find(1).to_param, :issue => {:parent_id => 1}
+  end
+
+  def test_add_subissue_with_invalid_parent_id_should_render_404
+    @request.session[:user_id] = 2
+    get( :add_subissue, :project_id => 1,
+         :issue => {
+           :tracker_id => 3,
+           :subject => "test_add_subissue",
+           :description => "test_add_subissue",
+           :priority_id => 5,
+           :estimated_hours => ''},
+         :parent_issue_id => 'invalid_id')
+    assert_template 'common/404', :status => 404
+  end
+
+  def test_add_subissue_with_a_private_parent_issue
+    @request.session[:user_id] = 3 # can't access Project 5
+    get( :add_subissue, :project_id => 5,
+         :issue => {
+           :tracker_id      => 3,
+           :priority_id     => 5,
+           :subject         => "test_add_private_subissue",
+           :description     => "test_add_private_subissue",
+           :estimated_hours => '' },
+         :parent_issue_id => 6)
+    assert_template 'common/404', :status => 404
+  end
+
+  def test_index_view_option_always_show_parents
+    @private_issue = Issue.find(4)
+    @child_issue = Issue.find(15)
+    @child_issue.move_to_child_of(@private_issue)
+
+    @request.session[:user_id] = 3
+    get( :index,
+         :project_id => 1,
+         :set_filter => 1,
+         :view_options => { :show_parents => "show_always"})
+    assert_response :success
+
+    assert_select 'table.issues' do
+      assert_select 'span.issue-subject-in-tree.issue-level-1', /child001/
+      assert_select 'span.issue-subject-in-tree.issue-level-2', /subchild001/
+      assert_select 'span', /root/
+      
+      # Hidden issue on a private project
+      assert_select 'tr#issue-4', :count => 0
+      assert_select 'td.subject', :text => /Issue on project 2/, :count => 0
+      assert_select 'tr.private-issue .issue-subject', /Private/
+    end
+  end
+
+  def test_index_view_option_organize_by_parent
+    @private_issue = Issue.find(4)
+    @child_issue = Issue.find(15)
+    @child_issue.move_to_child_of(@private_issue)
+    
+    @request.session[:user_id] = 3
+    get( :index,
+         :project_id => 1,
+         :set_filter => 1,
+         :view_options => { :show_parents => "organize_by_parent"})
+    assert_response :success
+
+    assert_select 'table.issues' do
+      assert_select '.issue-subject-in-tree.issue-level-1', /child001/
+      assert_select '.issue-subject-in-tree.issue-level-1', /child002/
+      assert_select '.issue-subject-in-tree.issue-level-2', /subchild001/
+      assert_select '.issue-subject-in-tree.issue-level-2', /subchild002/
+      assert_select 'tr.private-issue .issue-subject', /Private/
+    end
+  end
+
+  context "#auto_complete_for_issue_parent without authorization" do
+    setup do
+      @request.session[:user_id] = 3
+      get :auto_complete_for_issue_parent, :project_id => 2
+    end
+
+    should_respond_with 403
+  end
+
+  context "#auto_complete_for_issue_parent with authorization" do
+    setup do
+      @request.session[:user_id] = 3
+    end
+
+    context "with a missing phrase" do
+      setup do
+        get :auto_complete_for_issue_parent, :project_id => 1
+      end
+      
+      should_respond_with :success
+
+      should "have an hidden content body" do
+        assert_select 'li[style*=?]', /display:none/
+      end
+    end
+
+    context "with an issue number for the project" do
+      setup do
+        get :auto_complete_for_issue_parent, :project_id => 1, :issue_parent => '3'
+      end
+      
+      should_respond_with :success
+
+      should "have the matching issue in the content body" do
+        assert_select 'ul' do
+          assert_select 'li#3', /#{Issue.find(3).subject}/
+        end
+      end
+    end
+
+    context "with it's own issue number" do
+      setup do
+        get :auto_complete_for_issue_parent, :id => '3', :project_id => 1, :issue_parent => '3'
+      end
+      
+      should_respond_with :success
+
+      should "not show it's own issue as a result" do
+        assert_select 'ul' do
+          assert_select 'li#3', :count => 0
+        end
+      end
+    end
+
+    context "with a cross project issue number" do
+      setup do
+        Setting.cross_project_issue_relations = '1'
+        get :auto_complete_for_issue_parent, :project_id => 1, :issue_parent => '5'
+      end
+      
+      should_respond_with :success
+
+      should "have the matching issue in the content body" do
+        assert_select 'ul' do
+          assert_select 'li#5', /#{Issue.find(5).subject}/
+        end
+      end
+    end
+
+    context "searching by subject and description" do
+      setup do
+        get :auto_complete_for_issue_parent, :project_id => 1, :issue_parent => 'issue'
+      end
+      
+      should_respond_with :success
+
+      should "have the matching issues in the content body" do
+        assert_select 'ul' do
+          assert_select 'li', :count => 7
+        end
+      end
+
+    end
+
+    context "searching to unauthorized projects by issue id" do
+      setup do
+        get :auto_complete_for_issue_parent, :project_id => 1, :issue_parent => '4'
+      end
+      
+      should_respond_with :success
+
+      should "not contain the unauthorized issues" do
+        assert_select 'ul' do
+          assert_select 'li#4', :count => 0
+        end
+      end
+
+    end
+
+    context "searching to unauthorized projects by subject and description" do
+      setup do
+        get :auto_complete_for_issue_parent, :project_id => 1, :issue_parent => 'issue on project 2'
+      end
+      
+      should_respond_with :success
+
+      should "not contain the unauthorized issues" do
+        assert_select 'ul' do
+          assert_select 'li', :count => 7
+          assert_select 'li', :count => 0, :text => /issue on project 2/
+        end
+      end
+
+    end
+
+    should "limit results to 10 records" do
+      Setting.cross_project_issue_relations = '1'
+      get :auto_complete_for_issue_parent, :project_id => 1, :issue_parent => 'e'
+
+      assert_response :success
+      assert_select 'ul' do
+        assert_select 'li', :count => 10
+      end
+    end
+
+
+  end
 end
diff --git a/test/functional/projects_controller_test.rb b/test/functional/projects_controller_test.rb
index 6c88e41..1ceb13b 100644
--- a/test/functional/projects_controller_test.rb
+++ b/test/functional/projects_controller_test.rb
@@ -390,6 +390,9 @@ class ProjectsControllerTest < ActionController::TestCase
   end
 
   def test_post_destroy
+    # Remove the an open issue on a version
+    Issue.destroy(20)
+
     @request.session[:user_id] = 1 # admin
     post :destroy, :id => 1, :confirm => 1
     assert_redirected_to 'admin/projects'
@@ -612,6 +615,9 @@ class ProjectsControllerTest < ActionController::TestCase
   end
   
   def test_archive
+    # Remove the an open issue on a version
+    Issue.destroy(20)
+
     @request.session[:user_id] = 1 # admin
     post :archive, :id => 1
     assert_redirected_to 'admin/projects'
diff --git a/test/functional/queries_controller_test.rb b/test/functional/queries_controller_test.rb
index af2a86e..33c33ba 100644
--- a/test/functional/queries_controller_test.rb
+++ b/test/functional/queries_controller_test.rb
@@ -127,6 +127,23 @@ class QueriesControllerTest < ActionController::TestCase
     assert_equal [['due_date', 'desc'], ['tracker', 'asc']], query.sort_criteria
   end
   
+  def test_new_with_view_options
+    @request.session[:user_id] = 1
+    post :new,
+         :confirm => '1',
+         :default_columns => '1',
+         :operators => {"status_id" => "o"},
+         :values => {"status_id" => ["1"]},
+         :query => {:name => "test_new_with_view_options",
+                    :is_public => "1"},
+         :view_options => {:show_parents => 'organize_by_parent'}
+    
+    query = Query.find_by_name("test_new_with_view_options")
+    assert_not_nil query
+    assert query.view_options.key?('show_parents')
+    assert_equal "organize_by_parent", query.view_options['show_parents']
+  end
+  
   def test_get_edit_global_public_query
     @request.session[:user_id] = 1
     get :edit, :id => 4
diff --git a/test/integration/projects_test.rb b/test/integration/projects_test.rb
index 14175ea..c6250ba 100644
--- a/test/integration/projects_test.rb
+++ b/test/integration/projects_test.rb
@@ -21,6 +21,9 @@ class ProjectsTest < ActionController::IntegrationTest
   fixtures :projects, :users, :members
   
   def test_archive_project
+    # Remove the an open issue on a version
+    Issue.destroy(20)
+
     subproject = Project.find(1).children.first
     log_user("admin", "admin")
     get "admin/projects"
diff --git a/test/unit/enumeration_test.rb b/test/unit/enumeration_test.rb
index d9d3307..4f5a1f2 100644
--- a/test/unit/enumeration_test.rb
+++ b/test/unit/enumeration_test.rb
@@ -25,7 +25,7 @@ class EnumerationTest < ActiveSupport::TestCase
   
   def test_objects_count
     # low priority
-    assert_equal 6, Enumeration.find(4).objects_count
+    assert_equal 13, Enumeration.find(4).objects_count
     # urgent
     assert_equal 0, Enumeration.find(7).objects_count
   end
@@ -79,7 +79,7 @@ class EnumerationTest < ActiveSupport::TestCase
   def test_destroy_with_reassign
     Enumeration.find(4).destroy(Enumeration.find(6))
     assert_nil Issue.find(:first, :conditions => {:priority_id => 4})
-    assert_equal 6, Enumeration.find(6).objects_count
+    assert_equal 13, Enumeration.find(6).objects_count
   end
 
   def test_should_be_customizable
diff --git a/test/unit/issue_priority_test.rb b/test/unit/issue_priority_test.rb
index 51a6b82..0f43888 100644
--- a/test/unit/issue_priority_test.rb
+++ b/test/unit/issue_priority_test.rb
@@ -26,7 +26,7 @@ class IssuePriorityTest < ActiveSupport::TestCase
   
   def test_objects_count
     # low priority
-    assert_equal 6, IssuePriority.find(4).objects_count
+    assert_equal 13, IssuePriority.find(4).objects_count
     # urgent
     assert_equal 0, IssuePriority.find(7).objects_count
   end
diff --git a/test/unit/issue_test.rb b/test/unit/issue_test.rb
index b22b05c..96009fc 100644
--- a/test/unit/issue_test.rb
+++ b/test/unit/issue_test.rb
@@ -27,6 +27,10 @@ class IssueTest < ActiveSupport::TestCase
            :custom_fields, :custom_fields_projects, :custom_fields_trackers, :custom_values,
            :time_entries
 
+  def setup
+    Setting.reopened_parent_issue_status = ''
+  end
+
   def test_create
     issue = Issue.new(:project_id => 1, :tracker_id => 1, :author_id => 3, :status_id => 1, :priority => IssuePriority.all.first, :subject => 'test_create', :description => 'IssueTest#test_create', :estimated_hours => '1:30')
     assert issue.save
@@ -556,6 +560,40 @@ class IssueTest < ActiveSupport::TestCase
         assert_equal 50, @issue.done_ratio
       end
     end
+
+    context "on a leaf" do
+      should "return the attribute" do
+        issue = Issue.generate_for_project!(Project.find(1), :subject => 'is a leaf', :done_ratio => 50)
+        assert_equal 50, issue.done_ratio
+      end
+    end
+
+    context "not a leaf (has child issues)" do
+      should "total the estimated hours of the children" do
+        @issue1 = Issue.generate_for_project!(Project.find(1), :subject => 'parent', :done_ratio => 10, :start_date => '2009-12-07', :due_date => '2009-12-17')
+        @issue2 = Issue.generate_for_project!(Project.find(1), :subject => 'parent 2', :done_ratio => 20, :start_date => '2009-12-07', :due_date => '2009-12-17')
+        @issue3 = Issue.generate_for_project!(Project.find(1), :subject => 'c', :done_ratio => 30, :start_date => '2009-12-07', :due_date => '2009-12-17')
+        @issue4 = Issue.generate_for_project!(Project.find(1), :subject => 'c', :done_ratio => 40, :start_date => '2009-12-07', :due_date => '2009-12-17')
+        @issue5 = Issue.generate_for_project!(Project.find(1), :subject => 'c', :done_ratio => 50, :start_date => '2009-12-07', :due_date => '2009-12-17')
+
+        @issue2.move_to_child_of @issue1
+        @issue3.move_to_child_of @issue2
+        @issue4.move_to_child_of @issue2
+        @issue5.move_to_child_of @issue2
+        
+        [@issue1, @issue2, @issue3, @issue4, @issue5].each {|i| i.reload }
+
+        assert_equal 50, @issue5.done_ratio # Leaf
+        assert_equal 40, @issue4.done_ratio # Leaf
+        assert_equal 30, @issue3.done_ratio # Leaf
+        # Parent with three. Children are 12 actual days / 30 planned days
+        assert_equal 40, @issue2.done_ratio
+        # Grantparent with three. Children are 12 actual days / 30
+        # planned days.  Issue2 doesn't add anything since it's just a
+        # parent task for the leafs
+        assert_equal 40, @issue1.done_ratio
+      end
+    end
   end
 
   context "#update_done_ratio_from_issue_status" do
@@ -589,4 +627,280 @@ class IssueTest < ActiveSupport::TestCase
       end
     end
   end
+
+  context "#do_subtasks_hooks" do
+    should "update target version of parent issue" do
+      create_family_of_issues 'Target version of parent updates test'
+
+      version_2_0 = Version.find( versions( :versions_003).id)
+      version_2_1 = Version.find( versions( :versions_009).id)
+      
+      # set target version for child
+      @issue2.fixed_version = version_2_0
+      assert @issue2.save
+      assert @issue1.reload.fixed_version == @issue2.fixed_version
+
+      # set target version for child of child larger than target version
+      # of child. so, target version of parents should be updated.
+      @issue3.fixed_version = version_2_1
+      assert @issue3.save
+      assert @issue1.reload.fixed_version == @issue3.fixed_version
+    end
+
+    should "not allow closing the parent issue while one of the children is open" do
+      create_family_of_issues 'Closing parent issue when some children is open test.'
+
+      closed_status = issue_statuses( :issue_statuses_005)
+      @issue3.status = closed_status
+      assert @issue3.save
+
+      assert_raise ActiveRecord::RecordInvalid do
+        @issue1.reload.status = closed_status
+        @issue1.save!
+      end
+      
+    end
+
+    
+    context "change the status of parent when some children are open" do
+      should "to the default status if the Reopened Parent Issue Status setting is blank" do
+        with_settings :reopened_parent_issue_status => "" do
+          create_family_of_issues 'Changing status of parent from closed to open when some of children is open.'
+
+          open_status   = issue_statuses( :issue_statuses_001)
+          closed_status = issue_statuses( :issue_statuses_005)
+
+          @issue3.status = closed_status
+          @issue2.status = closed_status
+          @issue1.status = closed_status
+          assert @issue3.save
+          assert @issue2.save
+          assert @issue1.save
+          assert @issue1.reload.closed?
+          assert @issue2.reload.closed?
+          assert @issue3.reload.closed?
+
+          # set status of children to open status. this should update status
+          # of parent and set it to open state.
+          @issue2.status = open_status
+          assert @issue2.save
+          assert !@issue2.reload.closed?
+          assert !@issue1.reload.closed?
+          assert_equal IssueStatus.default, @issue1.status
+        end
+      end
+
+      should "to the configured Reopened Parent Issue Status" do
+        configured_status = IssueStatus.find(4)
+        assert !configured_status.is_closed?
+
+        with_settings :reopened_parent_issue_status => configured_status.id.to_s do
+          create_family_of_issues 'Changing status of parent from closed to open when some of children is open.'
+
+          open_status   = issue_statuses( :issue_statuses_001)
+          closed_status = issue_statuses( :issue_statuses_005)
+
+          @issue3.status = closed_status
+          @issue2.status = closed_status
+          @issue1.status = closed_status
+          assert @issue3.save
+          assert @issue2.save
+          assert @issue1.save
+          assert @issue1.reload.closed?
+          assert @issue2.reload.closed?
+          assert @issue3.reload.closed?
+
+          # set status of children to open status. this should update status
+          # of parent and set it to open state.
+          @issue2.status = open_status
+          assert @issue2.save
+          assert !@issue2.reload.closed?
+          assert !@issue1.reload.closed?
+          assert_equal configured_status, @issue1.status
+        end
+      end
+    end
+    
+    should "update Target Version of parent if the children have a bigger target version" do
+      create_family_of_issues 'Update target version of parent if children have bigger target version.'
+
+      # set parent version to 1.
+      @issue1.fixed_version = versions( :versions_003)
+      assert @issue1.save
+      assert @issue1.reload.fixed_version == versions( :versions_003)
+
+      # set children to version higher that parent.
+      @issue2.fixed_version = versions( :versions_009)
+      assert @issue2.save
+      assert @issue1.reload.fixed_version == versions( :versions_009)
+    end
+
+    should "set target version of parent if children have a target version" do
+      create_family_of_issues 'Update target version of parent if children have a target version.'
+
+      @issue2.fixed_version = versions( :versions_003)
+      assert @issue2.save, @issue2.errors.full_messages
+      assert @issue2.reload.fixed_version == versions( :versions_003)
+      assert @issue1.reload.fixed_version == @issue2.fixed_version
+    end
+
+    should "not allow to set target version of parent lower than any of the children" do
+      create_family_of_issues 'Not allowing to set target version of parent lower than any of the children.'
+
+      [ @issue1, @issue2, @issue3 ].each do |issue|
+        issue.update_attribute :fixed_version, versions( :versions_002)
+      end
+
+      assert_raise ActiveRecord::RecordInvalid do
+        @issue1.fixed_version = versions( :versions_001)
+        @issue1.save!
+      end
+    end
+
+    should "not set target version of parent if child on another project" do
+      create_family_of_issues 'Should not set target version of parnet if child on another project.'
+
+      @issue2.fixed_version = versions( :versions_003)
+      assert @issue2.save
+      assert @issue1.reload.fixed_version == versions( :versions_003)
+
+      online_store = projects( :projects_002)
+      assert @issue2.move_to( online_store)
+      # Need to retrieve the record again to bust the
+      # @assignable_versions cache
+      @issue2 = Issue.find(@issue2.id)
+      @issue2.fixed_version = versions( :onlinestore_1_0)
+      assert @issue2.save, @issue2.errors.full_messages
+      assert @issue1.reload.fixed_version == versions( :versions_003)
+      assert @issue2.reload.fixed_version == versions( :onlinestore_1_0)
+    end
+
+  end
+
+  def test_should_update_due_to_date_if_target_version_is_set_but_due_to_is_not
+    @issue = Issue.new( :project_id => 1, :tracker_id => 1,
+                        :author_id => 1, :status_id => 1,
+                        :priority => IssuePriority.first,
+                        :subject => 'issue for test hook which set due_to when sets target version.',
+                        :description => 'issue for test hook which set due_to when sets target version.')
+
+    assert @issue.save
+    assert @issue.reload.due_date == nil
+    @issue.fixed_version = versions(:versions_003)
+    assert @issue.save
+    assert @issue.reload.due_date == @issue.reload.fixed_version.due_date
+  end
+
+  def test_settings_delete_children_on
+    with_settings :delete_children => "1" do
+      @root = issues( :issues_root)
+      children_before_delete = @root.children.clone
+      assert @root.destroy, "failed to destroy parent issue"
+      assert_raise ActiveRecord::RecordNotFound do
+        children_before_delete.each( &:reload)
+      end
+    end
+  end
+  
+  def test_settings_delete_children_off
+    with_settings :delete_children => "0" do
+      @root = issues( :issues_root)
+      children_before_delete = @root.children.clone
+      assert @root.destroy, "failed to destroy parent issue"
+      assert_nothing_raised do
+        children_before_delete.each( &:reload)
+      end
+    end
+  end
+
+  def test_should_not_change_target_version_if_children_from_another_project
+    with_settings :cross_project_issue_relations => true do
+      @root = issues( :issues_root)
+      @issue_another_project = issues( :issue_leaf_from_another_project)
+
+      older_version = versions( :versions_001)
+      newer_version = versions( :onlinestore_1_0)
+      @root.update_attribute :fixed_version,  older_version
+      @issue_another_project.move_to_child_of @root
+      assert @root.reload.fixed_version == older_version
+    end
+  end
+
+  context "Issue#estimated_hours" do
+    context "on a leaf" do
+      should "return the attribute" do
+        issue = Issue.generate_for_project!(Project.find(1), :subject => 'is a leaf', :estimated_hours => 10.0)
+        assert_equal 10, issue.estimated_hours
+      end
+    end
+
+    context "not a leaf (has child issues)" do
+      should "total the estimated hours of the children" do
+        @issue1 = Issue.generate_for_project!(Project.find(1), :subject => 'parent', :estimated_hours => 5.0)
+        @issue2 = Issue.generate_for_project!(Project.find(1), :subject => 'parent 2', :estimated_hours => 10.0)
+        @issue3 = Issue.generate_for_project!(Project.find(1), :subject => 'parent', :estimated_hours => 20.0)
+        @issue4 = Issue.generate_for_project!(Project.find(1), :subject => 'parent', :estimated_hours => 20.0)
+        @issue5 = Issue.generate_for_project!(Project.find(1), :subject => 'parent', :estimated_hours => nil)
+
+        @issue2.move_to_child_of @issue1
+        @issue3.move_to_child_of @issue2
+        @issue4.move_to_child_of @issue2
+        @issue5.move_to_child_of @issue2
+        
+        [@issue1, @issue2, @issue3, @issue4, @issue5].each {|i| i.reload }
+
+        assert_equal nil, @issue5.estimated_hours # Leaf
+        assert_equal 20, @issue4.estimated_hours # Leaf
+        assert_equal 20, @issue3.estimated_hours # Leaf
+        assert_equal 40, @issue2.estimated_hours # Parent with two
+        assert_equal 40, @issue1.estimated_hours # Parent with two
+
+      end
+    end
+  end
+
+  context "#parent_id=" do
+    setup do
+      @issue1 = Issue.generate_for_project!(Project.find(1), :subject => 'parent')
+      @issue2 = Issue.generate_for_project!(Project.find(1), :subject => 'child')
+    end
+
+    should "set the parent issue by id" do
+      @issue1.parent_id=(@issue2.id)
+
+      assert_equal @issue2.id, @issue1.parent_id
+      assert_equal @issue2, @issue1.parent
+    end
+
+    should "not allow setting the parent issue to itself" do
+      @issue1.parent_id=(@issue1.id)
+
+      assert_equal nil, @issue1.parent_id
+      assert_equal nil, @issue1.parent
+
+    end
+  end
+  
+  private
+
+  def create_family_of_issues( subject)
+    project = Project.find(1)
+    @issue1 = Issue.generate_for_project!(project, :subject => subject, :description => subject)
+    @issue2 = Issue.generate_for_project!(project, :subject => subject, :description => subject)
+    @issue3 = Issue.generate_for_project!(project, :subject => subject, :description => subject)
+
+    # 2 is a child of 1 
+    @issue2.move_to_child_of @issue1
+    
+    # And 3 is a child of 2
+    @issue3.move_to_child_of @issue2
+    
+    @issue1.reload
+    @issue2.reload
+    @issue3.reload
+
+    assert @issue2.parent == @issue1
+    assert @issue2.children.include?( @issue3)
+  end
+
 end
diff --git a/test/unit/project_test.rb b/test/unit/project_test.rb
index c1fc843..cfd04ca 100644
--- a/test/unit/project_test.rb
+++ b/test/unit/project_test.rb
@@ -97,6 +97,9 @@ class ProjectTest < ActiveSupport::TestCase
   end
   
   def test_archive
+    # Remove the an open issue on a version
+    Issue.destroy(20)
+
     user = @ecookbook.members.first.user
     @ecookbook.archive
     @ecookbook.reload
@@ -120,6 +123,9 @@ class ProjectTest < ActiveSupport::TestCase
   end
   
   def test_unarchive
+    # Remove the an open issue on a version
+    Issue.destroy(20)
+
     user = @ecookbook.members.first.user
     @ecookbook.archive
     # A subproject of an archived project can not be unarchived
@@ -136,6 +142,9 @@ class ProjectTest < ActiveSupport::TestCase
   end
   
   def test_destroy
+    # Remove the an open issue on a version
+    Issue.destroy(20)
+    
     # 2 active members
     assert_equal 2, @ecookbook.members.size
     # and 1 is locked
@@ -237,7 +246,7 @@ class ProjectTest < ActiveSupport::TestCase
     
     # Move project out of the issue's hierarchy
     moved_project = Project.find(3)
-    moved_project.set_parent!(Project.find(2))
+    assert moved_project.set_parent!(Project.find(2))
     parent_issue.reload
     issue_with_local_fixed_version.reload
     issue_with_hierarchy_fixed_version.reload
@@ -309,12 +318,17 @@ class ProjectTest < ActiveSupport::TestCase
   end
   
   def test_rolled_up_trackers_should_ignore_archived_subprojects
+    # Remove the an open issue on a version
+    Issue.destroy(20)
+    
     parent = Project.find(1)
     parent.trackers = Tracker.find([1,2])
     child = parent.children.find(3)
     child.trackers = Tracker.find([1,3])
-    parent.children.each(&:archive)
-    
+    parent.children.each do |child|
+      assert child.archive, "Project #{child} did not archive"
+    end
+      
     assert_equal [1,2], parent.rolled_up_trackers.collect(&:id)
   end
   
@@ -373,26 +387,29 @@ class ProjectTest < ActiveSupport::TestCase
     child = parent.children.find(3)
     private_child = parent.children.find(5)
     
-    assert_equal [1,2,3], parent.version_ids.sort
+    assert_equal [1,2,3,9], parent.version_ids.sort
     assert_equal [4], child.version_ids
     assert_equal [6], private_child.version_ids
     assert_equal [7], Version.find_all_by_sharing('system').collect(&:id)
 
-    assert_equal 6, parent.shared_versions.size
+    assert_equal 7, parent.shared_versions.size
     parent.shared_versions.each do |version|
       assert_kind_of Version, version
     end
 
-    assert_equal [1,2,3,4,6,7], parent.shared_versions.collect(&:id).sort
+    assert_equal [1,2,3,4,6,7,9], parent.shared_versions.collect(&:id).sort
   end
 
   def test_shared_versions_should_ignore_archived_subprojects
+    # Remove the an open issue on a version
+    Issue.destroy(20)
+
     parent = Project.find(1)
     child = parent.children.find(3)
-    child.archive
+    assert child.archive
     parent.reload
     
-    assert_equal [1,2,3], parent.version_ids.sort
+    assert_equal [1,2,3,9], parent.version_ids.sort
     assert_equal [4], child.version_ids
     assert !parent.shared_versions.collect(&:id).include?(4)
   end
@@ -402,12 +419,12 @@ class ProjectTest < ActiveSupport::TestCase
     parent = Project.find(1)
     child = parent.children.find(5)
     
-    assert_equal [1,2,3], parent.version_ids.sort
+    assert_equal [1,2,3,9], parent.version_ids.sort
     assert_equal [6], child.version_ids
 
     versions = parent.shared_versions.visible(user)
     
-    assert_equal 4, versions.size
+    assert_equal 5, versions.size
     versions.each do |version|
       assert_kind_of Version, version
     end
@@ -567,7 +584,6 @@ class ProjectTest < ActiveSupport::TestCase
       assert_equal @source_project.issues.size, @project.issues.size
       @project.issues.each do |issue|
         assert issue.valid?
-        assert ! issue.assigned_to.blank?
         assert_equal @project, issue.project
       end
       
@@ -581,7 +597,7 @@ class ProjectTest < ActiveSupport::TestCase
       User.current = User.find(1)
       assigned_version = Version.generate!(:name => "Assigned Issues", :status => 'open')
       @source_project.versions << assigned_version
-      assert_equal 3, @source_project.versions.size
+      assert_equal 4, @source_project.versions.size
       Issue.generate_for_project!(@source_project,
                                   :fixed_version_id => assigned_version.id,
                                   :subject => "change the new issues to use the copied version",
diff --git a/test/unit/query_test.rb b/test/unit/query_test.rb
index 2044747..829948f 100644
--- a/test/unit/query_test.rb
+++ b/test/unit/query_test.rb
@@ -63,7 +63,7 @@ class QueryTest < ActiveSupport::TestCase
     query.add_filter('estimated_hours', '!*', [''])
     issues = find_issues_with_query(query)
     assert !issues.empty?
-    assert issues.all? {|i| !i.estimated_hours}
+    assert issues.all? {|i| !i.read_attribute(:estimated_hours)}
   end
 
   def test_operator_all
-- 
1.6.4.2


