kanbanworkflowstimelinescrumrubyroadmapproject-planningproject-managementopenprojectangularissue-trackerifcgantt-chartganttbug-trackerboardsbcf
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
178 lines
5.7 KiB
178 lines
5.7 KiB
require_dependency 'issue'
|
|
|
|
module ActiveRecord
|
|
module Acts
|
|
module List
|
|
def move_after(id)
|
|
insert_at 0 unless in_list?
|
|
|
|
begin
|
|
prev = self.class.find(id)
|
|
rescue ActiveRecord::RecordNotFound
|
|
prev = nil
|
|
end
|
|
|
|
# if it's the first story, move it to the 1st position
|
|
if !prev
|
|
move_to_top
|
|
|
|
# if its predecessor has no position (shouldn't happen), make it
|
|
# the last story
|
|
elsif !prev.in_list?
|
|
move_to_bottom
|
|
|
|
# there's a valid predecessor
|
|
else
|
|
my_pos = send(position_column).to_i
|
|
prev_pos = prev.send(position_column).to_i
|
|
insert_at(my_pos == 0 || my_pos > prev_pos ? prev_pos + 1 : prev_pos)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
module Backlogs
|
|
module IssuePatch
|
|
def self.included(base) # :nodoc:
|
|
base.extend(ClassMethods)
|
|
base.send(:include, InstanceMethods)
|
|
|
|
base.class_eval do
|
|
unloadable
|
|
|
|
alias_method_chain :move_to_project_without_transaction, :autolink
|
|
alias_method_chain :recalculate_attributes_for, :remaining_hours
|
|
after_save :task_follows_story
|
|
end
|
|
end
|
|
|
|
module ClassMethods
|
|
end
|
|
|
|
module InstanceMethods
|
|
def move_to_project_without_transaction_with_autolink(new_project, new_tracker = nil, options = {})
|
|
newissue = move_to_project_without_transaction_without_autolink(new_project, new_tracker, options)
|
|
|
|
if self.project_id == newissue.project_id and self.is_story? and newissue.is_story? and self.id != newissue.id
|
|
relation = IssueRelation.new :relation_type => IssueRelation::TYPE_DUPLICATES
|
|
relation.issue_from = self
|
|
relation.issue_to = newissue
|
|
relation.save
|
|
end
|
|
|
|
return newissue
|
|
end
|
|
|
|
def journalized_update_attributes!(attribs)
|
|
self.init_journal(User.current)
|
|
return self.update_attributes!(attribs)
|
|
end
|
|
|
|
def journalized_update_attributes(attribs)
|
|
self.init_journal(User.current)
|
|
return self.update_attributes(attribs)
|
|
end
|
|
|
|
def journalized_update_attribute(attrib, v)
|
|
self.init_journal(User.current)
|
|
self.update_attribute(attrib, v)
|
|
end
|
|
|
|
def is_story?
|
|
return (Story.trackers.include?(self.tracker_id) and self.root?)
|
|
end
|
|
|
|
def is_task?
|
|
return (self.parent_id && self.tracker_id == Task.tracker)
|
|
end
|
|
|
|
def story
|
|
return Issue.find(:first,
|
|
:conditions => [ "id = ? and tracker_id in (?)", self.root_id, Story.trackers ])
|
|
end
|
|
|
|
def blocks
|
|
# return issues that I block that aren't closed
|
|
return [] if closed?
|
|
relations_from.collect {|ir| ir.relation_type == 'blocks' && !ir.issue_to.closed? ? ir.issue_to : nil}.compact
|
|
end
|
|
|
|
def blockers
|
|
# return issues that block me
|
|
return [] if closed?
|
|
relations_to.collect {|ir| ir.relation_type == 'blocks' && !ir.issue_from.closed? ? ir.issue_from : nil}.compact
|
|
end
|
|
|
|
def velocity_based_estimate
|
|
return nil if !self.is_story? || ! self.story_points || self.story_points <= 0
|
|
|
|
v = self.project.scrum_statistics
|
|
return nil if ! v or ! v[:days_per_point]
|
|
|
|
return Integer(self.story_points * v[:days_per_point])
|
|
end
|
|
|
|
def recalculate_attributes_for_with_remaining_hours(issue_id)
|
|
recalculate_attributes_for_without_remaining_hours(issue_id)
|
|
|
|
if issue_id && p = Issue.find_by_id(issue_id)
|
|
if p.left != (p.right + 1) # this node has children
|
|
p.update_attribute(:remaining_hours, p.leaves.sum(:remaining_hours).to_f)
|
|
end
|
|
end
|
|
end
|
|
|
|
def task_follows_story
|
|
## automatically sets the tracker to the task tracker for
|
|
## any descendant of story, and follow the version_id
|
|
## Normally one of the _before_save hooks ought to take
|
|
## care of this, but appearantly neither root_id nor
|
|
## parent_id are set at that point
|
|
|
|
touched_sprint = nil
|
|
|
|
if self.is_story?
|
|
# raw sql here because it's efficient and not
|
|
# doing so causes an update loop when Issue calls
|
|
# update_parent
|
|
|
|
if not Task.tracker.nil?
|
|
tasks = self.descendants.collect{|t| connection.quote(t.id)}.join(",")
|
|
if tasks != ""
|
|
connection.execute("update issues set tracker_id=#{connection.quote(Task.tracker)}, fixed_version_id=#{connection.quote(self.fixed_version_id)} where id in (#{tasks})")
|
|
end
|
|
end
|
|
touched_sprint = self.fixed_version
|
|
|
|
elsif not Task.tracker.nil?
|
|
begin
|
|
story = self.story
|
|
if not story.nil?
|
|
connection.execute "update issues set tracker_id = #{connection.quote(Task.tracker)}, fixed_version_id = #{connection.quote(story.fixed_version_id)} where id = #{connection.quote(self.id)}"
|
|
touched_sprint = story.fixed_version
|
|
end
|
|
end
|
|
end
|
|
|
|
if not touched_sprint.nil?
|
|
touched_sprint.touch_burndown
|
|
end
|
|
|
|
if self.position.nil? && (self.is_story? || self.is_task?)
|
|
# MySQL still doesn't properly support subrequests, so split
|
|
# into 2 requests
|
|
lowest = 1
|
|
res = connection.execute "select coalesce(max(position)+1,1) from issues"
|
|
res.each { |row|
|
|
lowest = row[0]
|
|
}
|
|
connection.execute "update issues set position=#{connection.quote(lowest)} where id=#{connection.quote(self.id)}"
|
|
end
|
|
end
|
|
|
|
end
|
|
end
|
|
end
|
|
|
|
Issue.send(:include, Backlogs::IssuePatch) unless Issue.included_modules.include? Backlogs::IssuePatch
|
|
|