diff --git a/app/controllers/issues_controller.rb b/app/controllers/issues_controller.rb
index 156933d..bfbc6c0 100644
--- a/app/controllers/issues_controller.rb
+++ b/app/controllers/issues_controller.rb
@@ -18,10 +18,11 @@
class IssuesController < ApplicationController
menu_item :new_issue, :only => :new
- before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment]
+ before_filter :find_issue_and_project_id, :only => [ :show_children, :hide_children ]
+ before_filter :find_issue, :only => [:show, :edit, :reply, :destroy_attachment, :update_subject ]
before_filter :find_issues, :only => [:bulk_edit, :move, :destroy]
- before_filter :find_project, :only => [:new, :update_form, :preview, :gantt, :calendar]
- before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu]
+ before_filter :find_project, :only => [:new, :auto_complete_for_issue_parent, :add_subissue, :update_form, :preview, :gantt, :calendar]
+ before_filter :authorize, :except => [:index, :changes, :preview, :update_form, :context_menu, :show_children, :hide_children, :auto_complete_for_issue_parent ]
before_filter :find_optional_project, :only => [:index, :changes]
accept_key_auth :index, :changes
@@ -43,6 +44,89 @@ class IssuesController < ApplicationController
include SortHelper
include IssuesHelper
helper :timelog
+ include ActionView::Helpers::PrototypeHelper
+
+ def update_subject
+ @notes = params[:notes]
+ journal = @issue.init_journal(User.current, @notes)
+ @edit_allowed = User.current.allowed_to?(:edit_issues, @project)
+ # User can change issue attributes only if he has :edit permission or if a workflow transition is allowed
+ if (@edit_allowed || !@allowed_statuses.empty?) && params[:subject]
+ @issue.subject = params[:subject]
+ end
+ if @issue.save
+ if !journal.new_record?
+ # Only send notification if something was actually changed
+ flash[:notice] = l(:notice_successful_update)
+ Mailer.deliver_issue_edit(journal) if Setting.notified_events.include?('issue_updated')
+ end
+ render(:text => params[:subject])
+ end
+ rescue ActiveRecord::StaleObjectError
+ # Optimistic locking exception
+ flash.now[:error] = l(:notice_locking_conflict)
+ end
+
+ def show_children
+ retrieve_query
+ if @query.valid?
+ render :update do |page|
+ page.replace "issue-#{@issue.id}", :partial => 'show_children', :locals => {:issue => @issue, :query => @query}
+ end
+ end
+ end
+
+ def hide_children
+ retrieve_query #verificar necessidade
+ if @query.valid?
+ render :update do |page|
+ page.select(".issue-#{@issue.id}-child").each do |child|
+ child.remove
+ end
+ page.select("#issue-#{@issue.id} a.contract-link").each do |contractIcon|
+ contractIcon.replace link_to_remote(image_tag("expand.png", :class=>'expand-icon'), {:url => {:controller => 'issues', :action => "show_children", :id => @issue, :project_id => @project}}, :class=>'expand-link')
+ end
+ end
+ end
+ 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.active_children
+ end
+
+ # Try to find issue by id.
+ if @phrase.match(/^#?(\d+)$/)
+ if Setting.cross_project_issue_relations?
+ issue = Issue.find_by_id( $1)
+ else
+ issue = Issue.find_by_id_and_project_id( $1, projects_to_search.collect { |i| i.id})
+ end
+ @candidates = [ issue ] if issue
+ end
+
+ # If finding by id is fail, try to find by searching in subject
+ # and description.
+ if @candidates.empty?
+ # 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}%"}
+
+ @candidates, count = Issue.search( like_tokens, projects_to_search, :before => true)
+ end
+
+ render :inline => "<%= auto_complete_result_parent_issue( @candidates, @phrase) %>"
+ end
def index
sort_init "#{Issue.table_name}.id", "desc"
@@ -58,11 +142,18 @@ class IssuesController < ApplicationController
end
@issue_count = Issue.count(:include => [:status, :project], :conditions => @query.statement)
@issue_pages = Paginator.new self, @issue_count, limit, params['page']
- @issues = Issue.find :all, :order => sort_clause,
- :include => [ :assigned_to, :status, :tracker, :project, :priority, :category, :fixed_version ],
- :conditions => @query.statement,
- :limit => limit,
- :offset => @issue_pages.current.offset
+ @issues = Issue.find_with_parents( :all, :order => sort_clause,
+ :include => [ :assigned_to,
+ :status,
+ :tracker,
+ :project,
+ :priority,
+ :category,
+ :fixed_version ],
+ :conditions => @query.statement,
+ :limit => limit,
+ :offset => @issue_pages.current.offset)
+
respond_to do |format|
format.html { render :template => 'issues/index.rhtml', :layout => !request.xhr? }
format.atom { render_feed(@issues, :title => "#{@project || Setting.app_title}: #{l(:label_issue_plural)}") }
@@ -132,7 +223,8 @@ class IssuesController < ApplicationController
end
@issue.status = default_status
@allowed_statuses = ([default_status] + default_status.find_new_statuses_allowed_to(User.current.role_for_project(@project), @issue.tracker)).uniq
-
+ @parent_issue = Issue.find( params[:issue_parent_issue_id]) unless params[:issue_parent_issue_id].blank?
+
if request.get? || request.xhr?
@issue.start_date ||= Date.today
else
@@ -140,6 +232,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
if @issue.save
+ set_parent( @issue, params[:issue_parent_issue_id]) if params[:issue_parent_issue_id]
attach_files(@issue, params[:attachments])
flash[:notice] = l(:notice_successful_create)
Mailer.deliver_issue_add(@issue) if Setting.notified_events.include?('issue_added')
@@ -150,6 +243,18 @@ class IssuesController < ApplicationController
@priorities = Enumeration::get_values('IPRI')
render :layout => !request.xhr?
end
+
+ def add_subissue
+ if params[:issue_parent_issue_id].nil?
+ flash.now[:error] = 'No parent issue specified.'
+ render :nothing => true, :layout => true
+ return
+ else
+ redirect_to( :controller => 'issues', :action => 'new',
+ :issue_parent_issue_id => params[:issue_parent_issue_id])
+ return
+ end
+ end
# Attributes that can be updated on workflow transition (without :edit permission)
# TODO: make it configurable (at least per role)
@@ -177,6 +282,7 @@ class IssuesController < ApplicationController
attachments = attach_files(@issue, params[:attachments])
attachments.each {|a| journal.details << JournalDetail.new(:property => 'attachment', :prop_key => a.id, :value => a.filename)}
if (@time_entry.hours.nil? || @time_entry.valid?) && @issue.save
+ set_parent(@issue, params[:issue_parent_issue_id]) if params[:issue_parent_issue_id]
# Log spend time
if current_role.allowed_to?(:log_time)
@time_entry.save
@@ -236,6 +342,7 @@ class IssuesController < ApplicationController
call_hook(:controller_issues_bulk_edit_before_save, { :params => params, :issue => issue })
# Don't save any change to the issue if the user is not authorized to apply the requested status
if (status.nil? || (issue.status.new_status_allowed_to?(status, current_role, issue.tracker) && issue.status = status)) && issue.save
+ set_parent(issue, params[:issue_parent_issue_id]) unless params[:issue_parent_issue_id].blank?
# Send notification for each issue (if changed)
Mailer.deliver_issue_edit(journal) if journal.details.any? && Setting.notified_events.include?('issue_updated')
else
@@ -410,6 +517,7 @@ class IssuesController < ApplicationController
def update_form
@issue = Issue.new(params[:issue])
+ @parent_issue = @issue.parent
render :action => :new, :layout => false
end
@@ -423,11 +531,20 @@ class IssuesController < ApplicationController
private
def find_issue
@issue = Issue.find(params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
+ @parent_issue = @issue.parent
@project = @issue.project
rescue ActiveRecord::RecordNotFound
render_404
end
+ def find_issue_and_project_id
+ @issue = Issue.find( params[:id], :include => [:project, :tracker, :status, :author, :priority, :category])
+ @parent_issue = @issue.parent
+ @project = Project.find( params[:project_id])
+ rescue ActiveRecord::RecordNotFound
+ render_404
+ end
+
# Filter for bulk operations
def find_issues
@issues = Issue.find_all_by_id(params[:id] || params[:ids])
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index a7efa46..5fe55ae 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -42,6 +42,8 @@ class ProjectsController < ApplicationController
helper :repositories
include RepositoriesHelper
include ProjectsHelper
+ helper :versions
+ include VersionsHelper
# Lists visible projects
def index
@@ -271,4 +273,48 @@ private
@selected_tracker_ids = selectable_trackers.collect {|t| t.id.to_s }
end
end
+
+
+ def sort_as_tree(issues)
+ issues.sort!{|a,b| a.hierarchical_level <=> b.hierarchical_level}
+ @sorted_issues = []
+ issues.each do |issue|
+ if @sorted_issues.empty?
+ @sorted_issues << issue
+ next
+ end
+ @time_to_stop = false #indicates when this task reaches its parent task (important because it has to stop between its parent task and the next aunt task
+ @sorted_issues.each do |sorted_issue|
+ #if same parent and smaller date, stop; if same parent, same date and smaller id, stop; after parent and before next parent, stop;
+ if ((sorted_issue.parent == issue.parent) && (sorted_issue.start_date > issue.start_date)) ||
+ ((sorted_issue.parent == issue.parent) && (sorted_issue.start_date == issue.start_date) && (sorted_issue.id > issue.id)) ||
+ (@time_to_stop && (sorted_issue.hierarchical_level < issue.hierarchical_level))
+ @sorted_issues.insert(@sorted_issues.index(sorted_issue), issue)
+ break
+ end
+ @time_to_stop = true if sorted_issue == issue.parent
+ end
+ #if this issue's parent is the last element
+ @sorted_issues << issue if @time_to_stop
+ end
+ @sorted_issues
+ end
+
+ #assumes that first level issues are ordered by date (sort_as_tree)
+ def integrate_versions_with_issues_tree(issues, versions)
+ versions.sort! {|x,y| x.start_date <=> y.start_date }
+ versions.each do |version|
+ issues << version if issues.empty?
+ issues.each do |issue|
+ if ((issue.is_a? Issue && issue.root?) || (issue.is_a? Version)) && version.start_date < issue.start_date
+ #insert version before a root task or another version whose date is immediately after this task's one
+ issues.insert(issues.index(issue), version)
+ elsif issue == issues.last
+ issues << version
+ end
+ end
+ end
+ issues
+ end
+
end
diff --git a/app/controllers/versions_controller.rb b/app/controllers/versions_controller.rb
index 3a22217..d1dfec1 100644
--- a/app/controllers/versions_controller.rb
+++ b/app/controllers/versions_controller.rb
@@ -20,6 +20,10 @@ class VersionsController < ApplicationController
before_filter :find_project, :authorize
def show
+ @issues = @version.fixed_issues.find(:all,
+ :include => [:status, :tracker],
+ :order => "#{Tracker.table_name}.position, #{Issue.table_name}.id")
+ @issues = Issue.find_with_parents( @issues.collect { |i| i.id})
end
def edit
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 43acabd..94c57ad 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -19,6 +19,12 @@ require 'csv'
module IssuesHelper
include ApplicationHelper
+
+ def issue_ancestors(issue=@issue)
+ ancestors = ""
+ return "" if issue.parent == nil
+ ancestors += "issue-#{issue.parent.id}-child " + issue_ancestors(issue.parent)
+ end
def render_issue_tooltip(issue)
@cached_label_start_date ||= l(:field_start_date)
@@ -185,4 +191,31 @@ module IssuesHelper
export.rewind
export
end
+
+ def set_parent(issue, parent_id)
+ if (issue && parent_id.to_s.size > 0)
+ issue.relations_from.each do |relation|
+ if relation.relation_type == IssueRelation::TYPE_PARENTS
+ relation.destroy
+ end
+ end
+ issue.reload
+ IssueRelation.create do |relation|
+ relation.issue_from = issue
+ relation.issue_to = Issue.find(parent_id)
+ relation.relation_type = IssueRelation::TYPE_PARENTS
+ unless relation.save
+ flash[:error] = "Can't set ##{parent_id} as parent for the issue."
+ end
+ end unless parent_id == '0'
+ end
+ end
+
+ def auto_complete_result_parent_issue(candidates, phrase)
+ return "" if candidates.empty?
+ candidates.map! do |c|
+ content_tag("li", highlight( c.to_s, phrase), :id => String( c[:id]))
+ end
+ content_tag("ul", candidates.uniq)
+ end
end
diff --git a/app/helpers/queries_helper.rb b/app/helpers/queries_helper.rb
index cf9819f..f5b9042 100644
--- a/app/helpers/queries_helper.rb
+++ b/app/helpers/queries_helper.rb
@@ -1,3 +1,4 @@
+# -*- coding: mule-utf-8 -*-
# redMine - project management software
# Copyright (C) 2006-2007 Jean-Philippe Lang
#
@@ -40,8 +41,7 @@ module QueriesHelper
else
case column.name
when :subject
- h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') +
- link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
+ subject_in_tree(issue, value)
when :done_ratio
progress_bar(value, :width => '80px')
when :fixed_version
@@ -52,4 +52,22 @@ module QueriesHelper
end
end
end
+
+ def subject_in_tree(issue, value)
+ image = ""
+ unless issue.edge? #don't show + or - icons for leaf issues
+ #o controle das tarefas a ser mostradas não está sendo feito por _list? verificar
+ image = @show_all_issues ||
+ @show_children ?
+ link_to_remote(image_tag("contract.png", :class=>'contract-icon'), {:url => {:controller => 'issues', :action => "hide_children", :id => issue, :project_id => @project}}, :class=>'contract-link') :
+ link_to_remote(image_tag("expand.png", :class=>'expand-icon'), {:url => {:controller => 'issues', :action => "show_children", :id => issue, :project_id => @project}}, :class=>'expand-link')
+ end
+ content_tag('span', image + content_tag('div', subject_text(issue, value), :class=>'issue-subject'), :class=>"issue-subject-level-#{issue.hierarchical_level}")
+ end
+
+ def subject_text(issue, value)
+ subject_text = link_to(h(value), :controller => 'issues', :action => 'show', :id => issue)
+ h((@project.nil? || @project != issue.project) ? "#{issue.project.name} - " : '') + subject_text
+ end
+
end
diff --git a/app/helpers/versions_helper.rb b/app/helpers/versions_helper.rb
index 0fcc640..c50b20e 100644
--- a/app/helpers/versions_helper.rb
+++ b/app/helpers/versions_helper.rb
@@ -44,4 +44,31 @@ 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 = 1)
+ issues_on_current_level = issues.select { |i| i.hierarchical_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[:style] = 'opacity: 0.5;filter: alpha(opacity=50);'
+ 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) + ": " + h(issue.subject)
+ end
+ children_to_print = issues & issue.children
+ children_to_print += issues.select { |i| i.hierarchical_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 4701e41..0be2ecc 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -18,20 +18,20 @@
class Issue < ActiveRecord::Base
belongs_to :project
belongs_to :tracker
- belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
- belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
- belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
- belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
- belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
- belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
+ belongs_to :status, :class_name => 'IssueStatus', :foreign_key => 'status_id'
+ belongs_to :author, :class_name => 'User', :foreign_key => 'author_id'
+ belongs_to :assigned_to, :class_name => 'User', :foreign_key => 'assigned_to_id'
+ belongs_to :fixed_version, :class_name => 'Version', :foreign_key => 'fixed_version_id'
+ belongs_to :priority, :class_name => 'Enumeration', :foreign_key => 'priority_id'
+ belongs_to :category, :class_name => 'IssueCategory', :foreign_key => 'category_id'
- has_many :journals, :as => :journalized, :dependent => :destroy
- has_many :attachments, :as => :container, :dependent => :destroy
+ has_many :journals, :as => :journalized, :dependent => :destroy
+ has_many :attachments, :as => :container, :dependent => :destroy
has_many :time_entries, :dependent => :delete_all
has_and_belongs_to_many :changesets, :order => "#{Changeset.table_name}.committed_on ASC, #{Changeset.table_name}.id ASC"
has_many :relations_from, :class_name => 'IssueRelation', :foreign_key => 'issue_from_id', :dependent => :delete_all
- has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
+ has_many :relations_to, :class_name => 'IssueRelation', :foreign_key => 'issue_to_id', :dependent => :delete_all
acts_as_customizable
acts_as_watchable
@@ -44,9 +44,9 @@ class Issue < ActiveRecord::Base
acts_as_activity_provider :find_options => {:include => [:project, :author, :tracker]}
- validates_presence_of :subject, :description, :priority, :project, :tracker, :author, :status
- validates_length_of :subject, :maximum => 255
- validates_inclusion_of :done_ratio, :in => 0..100
+ validates_presence_of :subject, :description, :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
def after_initialize
@@ -99,11 +99,6 @@ class Issue < ActiveRecord::Base
return true
end
- def priority_id=(pid)
- self.priority = nil
- write_attribute(:priority_id, pid)
- end
-
def estimated_hours=(h)
write_attribute :estimated_hours, (h.is_a?(String) ? h.to_hours : h)
end
@@ -120,6 +115,19 @@ class Issue < ActiveRecord::Base
if start_date && soonest_start && start_date < soonest_start
errors.add :start_date, :activerecord_error_invalid
end
+
+ if IssueStatus.find_by_id( @attributes['status_id']).is_closed? && children.find { |i| !i.closed? }
+ errors.add :status, "Can't close parent issue while on of the children still is open."
+ end
+
+ unless children.empty?
+ 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, "Can't set target version of parent issue lower than any of the children."
+ end
+ end
+ end
end
def validate_on_create
@@ -140,7 +148,7 @@ class Issue < ActiveRecord::Base
@current_journal.details << JournalDetail.new(:property => 'attr',
:prop_key => c,
:old_value => @issue_before_change.send(c),
- :value => send(c)) unless send(c)==@issue_before_change.send(c)
+ :value => send(c)) unless send(c)==@issue_before_change.send(c) || (!self.edge? && %w(status_id priority_id fixed_version_id start_date due_date done_ratio estimated_hours).include?(c))
}
# custom fields changes
custom_values.each {|c|
@@ -153,6 +161,7 @@ class Issue < ActiveRecord::Base
}
@current_journal.save
end
+
# Save the issue even if the journal is not saved (because empty)
true
end
@@ -164,6 +173,14 @@ class Issue < ActiveRecord::Base
# Update start/due dates of following issues
relations_from.each(&:set_issue_to_dates)
+ # Set default status of parent if new status openes the issue.
+ relations_from.each do |relation|
+ if relation.relation_type == IssueRelation::TYPE_PARENTS
+ relation.set_issue_to_default_status
+ relation.set_issue_to_target_version
+ end
+ end
+
# Close duplicates if the issue was closed
if @issue_before_change && !@issue_before_change.closed? && self.closed?
duplicates.each do |duplicate|
@@ -177,7 +194,7 @@ class Issue < ActiveRecord::Base
end
end
end
-
+
def init_journal(user, notes = "")
@current_journal ||= Journal.new(:journalized => self, :user => user, :notes => notes)
@issue_before_change = self.clone
@@ -189,6 +206,11 @@ class Issue < ActiveRecord::Base
@current_journal
end
+ def priority_id=(pid)
+ self.priority = nil
+ write_attribute(:priority_id, pid)
+ end
+
# Return true if the issue is closed, otherwise false
def closed?
self.status.is_closed?
@@ -240,7 +262,11 @@ class Issue < ActiveRecord::Base
# Returns the due date or the target due date if any
# Used on gantt chart
def due_before
- due_date || (fixed_version ? fixed_version.effective_date : nil)
+ due_date || (fixed_version ? fixed_version.effective_date : 0)
+ end
+
+ def duration1
+ (start_date && due_date) ? (due_date - start_date + 1) : 0
end
def duration
@@ -257,6 +283,137 @@ class Issue < ActiveRecord::Base
end
end
+ def done_ratio
+ if children?
+ @total_planned_days ||= 0
+ @total_actual_days ||= 0
+ children.each do |child| # from every subtask get the total number of days and the number of days already "worked"
+ planned_days = child.duration1
+ actual_days = child.done_ratio ? (planned_days * child.done_ratio / 100).floor : 0
+ @total_planned_days += planned_days
+ @total_actual_days += actual_days
+ end
+ @total_done_ratio = @total_planned_days != 0 ? (@total_actual_days * 100 / @total_planned_days).floor : 0
+ else
+ read_attribute(:done_ratio)
+ end
+ end
+
+ def estimated_hours
+ if children?
+ is_set = false
+ children.each do |child|
+ if child.estimated_hours
+ if is_set
+ @est_hours += child.estimated_hours
+ else
+ @est_hours = child.estimated_hours
+ is_set = true
+ end
+ end
+ end
+ @est_hours
+ else
+ read_attribute(:estimated_hours)
+ end
+ end
+
+ def start_date
+ calculate 'start_date'
+ end
+
+ def due_date
+ calculate 'due_date'
+ end
+
+ def calculate(field)
+ if children?
+ @value = eval "children.first.#{field}"
+ children.each do |child|
+ case field
+ when 'start_date'
+ if child.start_date && (!@value || @value > child.start_date)
+ @value = child.start_date
+ end
+ when 'due_date'
+ if child.due_date && (!@value || @value < child.due_date)
+ @value = child.due_date
+ end
+ end
+ end
+ @value
+ else
+ read_attribute(eval(":#{field}"))
+ end
+ end
+
+ def children?
+ children != []
+ end
+
+ def children
+ children = []
+ relations_to.each do |relation|
+ if relation.relation_type == IssueRelation::TYPE_PARENTS
+ children << relation.other_issue(self)
+ end
+ end
+ children
+ end
+
+ def parent
+ relations_from.each do |relation|
+ if relation.relation_type == IssueRelation::TYPE_PARENTS
+ return parent = relation.other_issue(self)
+ end
+ end
+ return nil
+ end
+
+ def parent?
+ parent != nil
+ end
+
+ def root?
+ !parent?
+ end
+
+ def ancestors(issue=self)
+ a = []
+ return a if ! issue.parent?
+ (a << issue.parent) | ancestors(issue.parent)
+ end
+
+ #First level tasks have hierarchical level = 1 and so on
+ def hierarchical_level(issue=self)
+ issue.parent? ? (1 + hierarchical_level(issue.parent)) : 1
+ end
+
+ def edge?
+ relations_to.each do |relation|
+ if relation.relation_type == IssueRelation::TYPE_PARENTS
+ return false
+ end
+ end
+ return true
+ end
+
+ def orphan?
+ root? && edge?
+ end
+
+ def self.find_with_parents( *args)
+ issues = find( *args)
+ return [] if issues.empty?
+ issues.each do |i|
+ while not i.root?
+ issues += [ i.parent ]
+ i = i.parent
+ end
+ end
+ issues.uniq
+ end
+
def to_s
"#{tracker} ##{id}: #{subject}"
end
diff --git a/app/models/issue_relation.rb b/app/models/issue_relation.rb
index 49329e0..bc66321 100644
--- a/app/models/issue_relation.rb
+++ b/app/models/issue_relation.rb
@@ -17,23 +17,26 @@
class IssueRelation < ActiveRecord::Base
belongs_to :issue_from, :class_name => 'Issue', :foreign_key => 'issue_from_id'
- belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
+ belongs_to :issue_to, :class_name => 'Issue', :foreign_key => 'issue_to_id'
TYPE_RELATES = "relates"
TYPE_DUPLICATES = "duplicates"
TYPE_BLOCKS = "blocks"
TYPE_PRECEDES = "precedes"
+ TYPE_PARENTS = "parents"
- TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
- TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
- TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
- TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
+ TYPES = { TYPE_RELATES => { :name => :label_relates_to, :sym_name => :label_relates_to, :order => 1 },
+ TYPE_DUPLICATES => { :name => :label_duplicates, :sym_name => :label_duplicated_by, :order => 2 },
+ TYPE_BLOCKS => { :name => :label_blocks, :sym_name => :label_blocked_by, :order => 3 },
+ TYPE_PRECEDES => { :name => :label_precedes, :sym_name => :label_follows, :order => 4 },
+ TYPE_PARENTS => { :name => :label_parents, :sym_name => :label_children, :order => 5 },
}.freeze
- validates_presence_of :issue_from, :issue_to, :relation_type
- validates_inclusion_of :relation_type, :in => TYPES.keys
- validates_numericality_of :delay, :allow_nil => true
- validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
+ validates_presence_of :issue_from, :issue_to, :relation_type
+ validates_inclusion_of :relation_type, :in => TYPES.keys
+ validates_uniqueness_of :relation_type, :scope => [:issue_from_id, :relation_type], :message=>l(:error_issue_can_have_only_one_parent)
+ validates_numericality_of :delay, :allow_nil => true
+ validates_uniqueness_of :issue_to_id, :scope => :issue_from_id
def validate
if issue_from && issue_to
@@ -57,8 +60,30 @@ class IssueRelation < ActiveRecord::Base
else
self.delay = nil
end
+
+ # Set new status to parent if new status openes the issue.
+ if TYPE_PARENTS == relation_type
+ set_issue_to_default_status
+ set_issue_to_target_version
+ end
+
set_issue_to_dates
end
+
+ def set_issue_to_default_status
+ if issue_to.closed? && !issue_from.closed?
+ issue_to.update_attribute :status, IssueStatus.default
+ end
+ end
+
+ def set_issue_to_target_version
+ if issue_to.fixed_version.nil? && issue_from.fixed_version or
+ ( issue_to.fixed_version && issue_from.fixed_version and
+ issue_to.fixed_version.project == issue_from.fixed_version.project and
+ issue_to.fixed_version < issue_from.fixed_version )
+ issue_to.update_attribute :fixed_version, issue_from.fixed_version
+ end
+ end
def set_issue_to_dates
soonest_start = self.successor_soonest_start
diff --git a/app/models/version.rb b/app/models/version.rb
index e379f4b..554ebd8 100644
--- a/app/models/version.rb
+++ b/app/models/version.rb
@@ -26,6 +26,8 @@ class Version < ActiveRecord::Base
validates_length_of :name, :maximum => 60
validates_format_of :effective_date, :with => /^\d{4}-\d{2}-\d{2}$/, :message => 'activerecord_error_not_a_date', :allow_nil => true
+ include Comparable
+
def start_date
effective_date
end
diff --git a/app/views/issues/_edit.rhtml b/app/views/issues/_edit.rhtml
index 2c7a428..b52940a 100644
--- a/app/views/issues/_edit.rhtml
+++ b/app/views/issues/_edit.rhtml
@@ -15,7 +15,7 @@
<%= render :partial => (@edit_allowed ? 'form' : 'form_update'), :locals => {:f => f} %>
<% end %>
- <% if authorize_for('timelog', 'edit') %>
+ <% if authorize_for('timelog', 'edit') && @issue.edge? %>