Rewrite the Gantt chart. #6276

This version of the Gantt chart supports nested charts. So Projects,
Versions, and Issues will be nested underneath their parents correctly.

Additional features:

* Move all Gantt code to Redmine::Helpers::Gantt class instead of having it in
  the Gantt class, controller, and view
* Recursive and nest sub-projects
* Recursive and nest versions
* Recursive and nest issues
* Draw a line showing when a Project is active and it's progress
* Draw a line showing when a Version is active and it's progress
* Show a version's % complete
* Change the color of Projects, Versions, and Issues if they are late or
  behind schedule
* Added Project#start_date and #due_date
* Added Project#completed_percent
* Use a mini-gravatar on the Gantt chart
* Added tests for the Gantt rendering

git-svn-id: svn+ssh://rubyforge.org/var/svn/redmine/trunk@4072 e93f8b46-1217-0410-a6f0-8f06a7374b81
This commit is contained in:
Eric Davis 2010-09-10 03:09:02 +00:00
parent 8d52608dba
commit bdb3937e0f
29 changed files with 1879 additions and 403 deletions

View File

@ -4,6 +4,7 @@ class GanttsController < ApplicationController
rescue_from Query::StatementInvalid, :with => :query_statement_invalid
helper :gantt
helper :issues
helper :projects
helper :queries
@ -14,32 +15,17 @@ class GanttsController < ApplicationController
def show
@gantt = Redmine::Helpers::Gantt.new(params)
@gantt.project = @project
retrieve_query
@query.group_by = nil
if @query.valid?
events = []
# Issues that have start and due dates
events += @query.issues(:include => [:tracker, :assigned_to, :priority],
:order => "start_date, due_date",
:conditions => ["(((start_date>=? and start_date<=?) or (due_date>=? and due_date<=?) or (start_date<? and due_date>?)) and start_date is not null and due_date is not null)", @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to, @gantt.date_from, @gantt.date_to]
)
# Issues that don't have a due date but that are assigned to a version with a date
events += @query.issues(:include => [:tracker, :assigned_to, :priority, :fixed_version],
: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]
)
# Versions
events += @query.versions(:conditions => ["effective_date BETWEEN ? AND ?", @gantt.date_from, @gantt.date_to])
@gantt.events = events
end
@gantt.query = @query if @query.valid?
basename = (@project ? "#{@project.identifier}-" : '') + 'gantt'
respond_to do |format|
format.html { render :action => "show", :layout => !request.xhr? }
format.png { send_data(@gantt.to_image(@project), :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(gantt_to_pdf(@gantt, @project), :type => 'application/pdf', :filename => "#{basename}.pdf") }
format.png { send_data(@gantt.to_image, :disposition => 'inline', :type => 'image/png', :filename => "#{basename}.png") } if @gantt.respond_to?('to_image')
format.pdf { send_data(@gantt.to_pdf, :type => 'application/pdf', :filename => "#{basename}.pdf") }
end
end

View File

@ -47,6 +47,7 @@ class IssuesController < ApplicationController
include SortHelper
include IssuesHelper
helper :timelog
helper :gantt
include Redmine::Export::PDF
verify :method => [:post, :delete],

View File

@ -122,6 +122,11 @@ module ApplicationHelper
link_to(text, {:controller => 'repositories', :action => 'revision', :id => project, :rev => revision}, :title => l(:label_revision_id, revision))
end
def link_to_project(project, options={})
options[:class] ||= 'project'
link_to(h(project), {:controller => 'projects', :action => 'show', :id => project}, :class => options[:class])
end
# Generates a link to a project if active
# Examples:
#
@ -832,6 +837,8 @@ module ApplicationHelper
email = $1
end
return gravatar(email.to_s.downcase, options) unless email.blank? rescue nil
else
''
end
end

View File

@ -0,0 +1,24 @@
# redMine - project management software
# Copyright (C) 2006 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
module GanttHelper
def number_of_issues_on_versions(gantt)
versions = gantt.events.collect {|event| (event.is_a? Version) ? event : nil}.compact
versions.sum {|v| v.fixed_issues.for_gantt.with_query(@query).count}
end
end

View File

@ -35,8 +35,10 @@ module IssuesHelper
@cached_label_due_date ||= l(:field_due_date)
@cached_label_assigned_to ||= l(:field_assigned_to)
@cached_label_priority ||= l(:field_priority)
@cached_label_project ||= l(:field_project)
link_to_issue(issue) + "<br /><br />" +
"<strong>#{@cached_label_project}</strong>: #{link_to_project(issue.project)}<br />" +
"<strong>#{@cached_label_status}</strong>: #{issue.status.name}<br />" +
"<strong>#{@cached_label_start_date}</strong>: #{format_date(issue.start_date)}<br />" +
"<strong>#{@cached_label_due_date}</strong>: #{format_date(issue.due_date)}<br />" +

View File

@ -62,10 +62,28 @@ class Issue < ActiveRecord::Base
named_scope :open, :conditions => ["#{IssueStatus.table_name}.is_closed = ?", false], :include => :status
named_scope :recently_updated, :order => "#{self.table_name}.updated_on DESC"
named_scope :recently_updated, :order => "#{Issue.table_name}.updated_on DESC"
named_scope :with_limit, lambda { |limit| { :limit => limit} }
named_scope :on_active_project, :include => [:status, :project, :tracker],
:conditions => ["#{Project.table_name}.status=#{Project::STATUS_ACTIVE}"]
named_scope :for_gantt, lambda {
{
:include => [:tracker, :status, :assigned_to, :priority, :project, :fixed_version],
:order => "#{Issue.table_name}.due_date ASC, #{Issue.table_name}.start_date ASC, #{Issue.table_name}.id ASC"
}
}
named_scope :without_version, lambda {
{
:conditions => { :fixed_version_id => nil}
}
}
named_scope :with_query, lambda {|query|
{
:conditions => Query.merge_conditions(query.statement)
}
}
before_create :default_assign
before_save :reschedule_following_issues, :close_duplicates, :update_done_ratio_from_issue_status
@ -358,6 +376,13 @@ class Issue < ActiveRecord::Base
!due_date.nil? && (due_date < Date.today) && !status.is_closed?
end
# Is the amount of work done less than it should for the due date
def behind_schedule?
return false if start_date.nil? || due_date.nil?
done_date = start_date + ((due_date - start_date+1)* done_ratio/100).floor
return done_date <= Date.today
end
# Users the issue can be assigned to
def assignable_users
project.assignable_users

View File

@ -413,6 +413,50 @@ class Project < ActiveRecord::Base
description.gsub(/^(.{#{length}}[^\n\r]*).*$/m, '\1...').strip if description
end
# The earliest start date of a project, based on it's issues and versions
def start_date
if module_enabled?(:issue_tracking)
[
issues.minimum('start_date'),
shared_versions.collect(&:effective_date),
shared_versions.collect {|v| v.fixed_issues.minimum('start_date')}
].flatten.compact.min
end
end
# The latest due date of an issue or version
def due_date
if module_enabled?(:issue_tracking)
[
issues.maximum('due_date'),
shared_versions.collect(&:effective_date),
shared_versions.collect {|v| v.fixed_issues.maximum('due_date')}
].flatten.compact.max
end
end
def overdue?
active? && !due_date.nil? && (due_date < Date.today)
end
# Returns the percent completed for this project, based on the
# progress on it's versions.
def completed_percent(options={:include_subprojects => false})
if options.delete(:include_subprojects)
total = self_and_descendants.collect(&:completed_percent).sum
total / self_and_descendants.count
else
if versions.count > 0
total = versions.collect(&:completed_pourcent).sum
total / versions.count
else
100
end
end
end
# Return true if this project is allowed to do the specified action.
# action can be:
# * a parameter-like Hash (eg. :controller => 'projects', :action => 'edit')

View File

@ -74,6 +74,18 @@ class Version < ActiveRecord::Base
effective_date && (effective_date <= Date.today) && (open_issues_count == 0)
end
def behind_schedule?
if completed_pourcent == 100
return false
elsif due_date && fixed_issues.present? && fixed_issues.minimum('start_date') # TODO: should use #start_date but that method is wrong...
start_date = fixed_issues.minimum('start_date')
done_date = start_date + ((due_date - start_date+1)* completed_pourcent/100).floor
return done_date <= Date.today
else
false # No issues so it's not late
end
end
# Returns the completion percentage of this version based on the amount of open/closed issues
# and the time spent on the open issues.
def completed_pourcent

View File

@ -1,3 +1,4 @@
<% @gantt.view = self %>
<h2><%= l(:label_gantt) %></h2>
<% form_tag(gantt_path(:month => params[:month], :year => params[:year], :months => params[:months]), :method => :put, :id => 'query_form') do %>
@ -55,11 +56,12 @@ if @gantt.zoom >1
end
end
# Width of the entire chart
g_width = (@gantt.date_to - @gantt.date_from + 1)*zoom
g_height = [(20 * @gantt.events.length + 6)+150, 206].max
# Collect the number of issues on Versions
g_height = [(20 * (@gantt.number_of_rows + 6))+150, 206].max
t_height = g_height + headers_height
%>
<table width="100%" style="border:0; border-collapse: collapse;">
<tr>
<td style="width:<%= subject_width %>px; padding:0px;">
@ -67,26 +69,10 @@ t_height = g_height + headers_height
<div style="position:relative;height:<%= t_height + 24 %>px;width:<%= subject_width + 1 %>px;">
<div style="right:-2px;width:<%= subject_width %>px;height:<%= headers_height %>px;background: #eee;" class="gantt_hdr"></div>
<div style="right:-2px;width:<%= subject_width %>px;height:<%= t_height %>px;border-left: 1px solid #c0c0c0;overflow:hidden;" class="gantt_hdr"></div>
<%
#
# Tasks subjects
#
top = headers_height + 8
@gantt.events.each do |i|
left = 4 + (i.is_a?(Issue) ? i.level * 16 : 0)
%>
<div style="position: absolute;line-height:1.2em;height:16px;top:<%= top %>px;left:<%= left %>px;overflow:hidden;"><small>
<% if i.is_a? Issue %>
<%= h("#{i.project} -") unless @project && @project == i.project %>
<%= link_to_issue i %>
<% else %>
<span class="icon icon-package">
<%= link_to_version i %>
</span>
<% end %>
</small></div>
<% top = top + 20
end %>
<% top = headers_height + 8 %>
<%= @gantt.subjects(:headers_height => headers_height, :top => top, :g_width => g_width) %>
</div>
</td>
<td>
@ -164,53 +150,9 @@ if show_days
end
end %>
<%
#
# Tasks
#
top = headers_height + 10
@gantt.events.each do |i|
if i.is_a? Issue
i_start_date = (i.start_date >= @gantt.date_from ? i.start_date : @gantt.date_from )
i_end_date = (i.due_before <= @gantt.date_to ? i.due_before : @gantt.date_to )
<% top = headers_height + 10 %>
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @gantt.date_from ? @gantt.date_from : i_done_date )
i_done_date = (i_done_date >= @gantt.date_to ? @gantt.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - @gantt.date_from)*zoom).floor
i_width = ((i_end_date - i_start_date + 1)*zoom).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - i_start_date)*zoom).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor - 2 : 0 # delay width
css = "task " + (i.leaf? ? 'leaf' : 'parent')
%>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;" class="<%= css %> task_todo"><div class="left"></div>&nbsp;<div class="right"></div></div>
<% if l_width > 0 %>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= l_width %>px;" class="<%= css %> task_late">&nbsp;</div>
<% end %>
<% if d_width > 0 %>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:<%= d_width %>px;" class="<%= css %> task_done">&nbsp;</div>
<% end %>
<div style="top:<%= top %>px;left:<%= i_left + i_width + 8 %>px;background:#fff;" class="<%= css %>">
<%= i.status.name %>
<%= (i.done_ratio).to_i %>%
</div>
<div class="tooltip" style="position: absolute;top:<%= top %>px;left:<%= i_left %>px;width:<%= i_width %>px;height:12px;">
<span class="tip">
<%= render_issue_tooltip i %>
</span></div>
<% else
i_left = ((i.start_date - @gantt.date_from)*zoom).floor
%>
<div style="top:<%= top %>px;left:<%= i_left %>px;width:15px;" class="task milestone">&nbsp;</div>
<div style="top:<%= top %>px;left:<%= i_left + 12 %>px;background:#fff;" class="task">
<strong><%= format_version_name i %></strong>
</div>
<% end %>
<% top = top + 20
end %>
<%= @gantt.lines(:top => top, :zoom => zoom, :g_width => g_width ) %>
<%
#

View File

@ -312,186 +312,6 @@ module Redmine
pdf.Output
end
# Returns a PDF string of a gantt chart
def gantt_to_pdf(gantt, project)
pdf = IFPDF.new(current_language)
pdf.SetTitle("#{l(:label_gantt)} #{project}")
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage("L")
pdf.SetFontStyle('B',12)
pdf.SetX(15)
pdf.Cell(70, 20, project.to_s)
pdf.Ln
pdf.SetFontStyle('B',9)
subject_width = 100
header_heigth = 5
headers_heigth = header_heigth
show_weeks = false
show_days = false
if gantt.months < 7
show_weeks = true
headers_heigth = 2*header_heigth
if gantt.months < 3
show_days = true
headers_heigth = 3*header_heigth
end
end
g_width = 280 - subject_width
zoom = (g_width) / (gantt.date_to - gantt.date_from + 1)
g_height = 120
t_height = g_height + headers_heigth
y_start = pdf.GetY
# Months headers
month_f = gantt.date_from
left = subject_width
height = header_heigth
gantt.months.times do
width = ((month_f >> 1) - month_f) * zoom
pdf.SetY(y_start)
pdf.SetX(left)
pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
left = left + width
month_f = month_f >> 1
end
# Weeks headers
if show_weeks
left = subject_width
height = header_heigth
if gantt.date_from.cwday == 1
# gantt.date_from is monday
week_f = gantt.date_from
else
# find next monday after gantt.date_from
week_f = gantt.date_from + (7 - gantt.date_from.cwday + 1)
width = (7 - gantt.date_from.cwday + 1) * zoom-1
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width + 1, height, "", "LTR")
left = left + width+1
end
while week_f <= gantt.date_to
width = (week_f + 6 <= gantt.date_to) ? 7 * zoom : (gantt.date_to - week_f + 1) * zoom
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
left = left + width
week_f = week_f+7
end
end
# Days headers
if show_days
left = subject_width
height = header_heigth
wday = gantt.date_from.cwday
pdf.SetFontStyle('B',7)
(gantt.date_to - gantt.date_from + 1).to_i.times do
width = zoom
pdf.SetY(y_start + 2 * header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
left = left + width
wday = wday + 1
wday = 1 if wday > 7
end
end
pdf.SetY(y_start)
pdf.SetX(15)
pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
# Tasks
top = headers_heigth + y_start
pdf.SetFontStyle('B',7)
gantt.events.each do |i|
pdf.SetY(top)
pdf.SetX(15)
text = ""
if i.is_a? Issue
text = "#{i.tracker} #{i.id}: #{i.subject}"
else
text = i.name
end
text = "#{i.project} - #{text}" unless project && project == i.project
pdf.Cell(subject_width-15, 5, text, "LR")
pdf.SetY(top + 0.2)
pdf.SetX(subject_width)
pdf.SetFillColor(255, 255, 255)
pdf.Cell(g_width, 4.6, "", "LR", 0, "", 1)
pdf.SetY(top+1.5)
if i.is_a? Issue
i_start_date = (i.start_date >= gantt.date_from ? i.start_date : gantt.date_from )
i_end_date = (i.due_before <= gantt.date_to ? i.due_before : gantt.date_to )
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= gantt.date_from ? gantt.date_from : i_done_date )
i_done_date = (i_done_date >= gantt.date_to ? gantt.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - gantt.date_from)*zoom)
i_width = ((i_end_date - i_start_date + 1)*zoom)
d_width = ((i_done_date - i_start_date)*zoom)
l_width = ((i_late_date - i_start_date+1)*zoom) if i_late_date
l_width ||= 0
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(200,200,200)
pdf.Cell(i_width, 2, "", 0, 0, "", 1)
if l_width > 0
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(255,100,100)
pdf.Cell(l_width, 2, "", 0, 0, "", 1)
end
if d_width > 0
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(100,100,255)
pdf.Cell(d_width, 2, "", 0, 0, "", 1)
end
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left + i_width)
pdf.Cell(30, 2, "#{i.status} #{i.done_ratio}%")
else
i_left = ((i.start_date - gantt.date_from)*zoom)
pdf.SetX(subject_width + i_left)
pdf.SetFillColor(50,200,50)
pdf.Cell(2, 2, "", 0, 0, "", 1)
pdf.SetY(top+1.5)
pdf.SetX(subject_width + i_left + 3)
pdf.Cell(30, 2, "#{i.name}")
end
top = top + 5
pdf.SetDrawColor(200, 200, 200)
pdf.Line(15, top, subject_width+g_width, top)
if pdf.GetY() > 180
pdf.AddPage("L")
top = 20
pdf.Line(15, top, subject_width+g_width, top)
end
pdf.SetDrawColor(0, 0, 0)
end
pdf.Line(15, top, subject_width+g_width, top)
pdf.Output
end
end
end
end

View File

@ -19,11 +19,28 @@ module Redmine
module Helpers
# Simple class to handle gantt chart data
class Gantt
attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months, :events
include ERB::Util
include Redmine::I18n
# :nodoc:
# Some utility methods for the PDF export
class PDF
MaxCharactorsForSubject = 45
TotalWidth = 280
LeftPaneWidth = 100
def self.right_pane_width
TotalWidth - LeftPaneWidth
end
end
attr_reader :year_from, :month_from, :date_from, :date_to, :zoom, :months
attr_accessor :query
attr_accessor :project
attr_accessor :view
def initialize(options={})
options = options.dup
@events = []
if options[:year] && options[:year].to_i >0
@year_from = options[:year].to_i
@ -52,31 +69,6 @@ module Redmine
@date_to = (@date_from >> @months) - 1
end
def events=(e)
@events = e
# Adds all ancestors
root_ids = e.select {|i| i.is_a?(Issue) && i.parent_id? }.collect(&:root_id).uniq
if root_ids.any?
# Retrieves all nodes
parents = Issue.find_all_by_root_id(root_ids, :conditions => ["rgt - lft > 1"])
# Only add ancestors
@events += parents.select {|p| @events.detect {|i| i.is_a?(Issue) && p.is_ancestor_of?(i)}}
end
@events.uniq!
# Sort issues by hierarchy and start dates
@events.sort! {|x,y|
if x.is_a?(Issue) && y.is_a?(Issue)
gantt_issue_compare(x, y, @events)
else
gantt_start_compare(x, y)
end
}
# Removes issues that have no start or end date
@events.reject! {|i| i.is_a?(Issue) && (i.start_date.nil? || i.due_before.nil?) }
@events
end
def params
{ :zoom => zoom, :year => year_from, :month => month_from, :months => months }
end
@ -89,9 +81,651 @@ module Redmine
{ :year => (date_from >> months).year, :month => (date_from >> months).month, :zoom => zoom, :months => months }
end
### Extracted from the HTML view/helpers
# Returns the number of rows that will be rendered on the Gantt chart
def number_of_rows
if @project
return number_of_rows_on_project(@project)
else
Project.roots.inject(0) do |total, project|
total += number_of_rows_on_project(project)
end
end
end
# Returns the number of rows that will be used to list a project on
# the Gantt chart. This will recurse for each subproject.
def number_of_rows_on_project(project)
# Remove the project requirement for Versions because it will
# restrict issues to only be on the current project. This
# ends up missing issues which are assigned to shared versions.
@query.project = nil if @query.project
# One Root project
count = 1
# Issues without a Version
count += project.issues.for_gantt.without_version.with_query(@query).count
# Versions
count += project.versions.count
# Issues on the Versions
project.versions.each do |version|
count += version.fixed_issues.for_gantt.with_query(@query).count
end
# Subprojects
project.children.each do |subproject|
count += number_of_rows_on_project(subproject)
end
count
end
# Renders the subjects of the Gantt chart, the left side.
def subjects(options={})
options = {:indent => 4, :render => :subject, :format => :html}.merge(options)
output = ''
if @project
output << render_project(@project, options)
else
Project.roots.each do |project|
output << render_project(project, options)
end
end
output
end
# Renders the lines of the Gantt chart, the right side
def lines(options={})
options = {:indent => 4, :render => :line, :format => :html}.merge(options)
output = ''
if @project
output << render_project(@project, options)
else
Project.roots.each do |project|
output << render_project(project, options)
end
end
output
end
def render_project(project, options={})
options[:top] = 0 unless options.key? :top
options[:indent_increment] = 20 unless options.key? :indent_increment
options[:top_increment] = 20 unless options.key? :top_increment
output = ''
# Project Header
project_header = if options[:render] == :subject
subject_for_project(project, options)
else
# :line
line_for_project(project, options)
end
output << project_header if options[:format] == :html
options[:top] += options[:top_increment]
options[:indent] += options[:indent_increment]
# Second, Issues without a version
issues = project.issues.for_gantt.without_version.with_query(@query)
if issues
issue_rendering = render_issues(issues, options)
output << issue_rendering if options[:format] == :html
end
# Third, Versions
project.versions.sort.each do |version|
version_rendering = render_version(version, options)
output << version_rendering if options[:format] == :html
end
# Fourth, subprojects
project.children.each do |project|
subproject_rendering = render_project(project, options)
output << subproject_rendering if options[:format] == :html
end
# Remove indent to hit the next sibling
options[:indent] -= options[:indent_increment]
output
end
def render_issues(issues, options={})
output = ''
issues.each do |i|
issue_rendering = if options[:render] == :subject
subject_for_issue(i, options)
else
# :line
line_for_issue(i, options)
end
output << issue_rendering if options[:format] == :html
options[:top] += options[:top_increment]
end
output
end
def render_version(version, options={})
output = ''
# Version header
version_rendering = if options[:render] == :subject
subject_for_version(version, options)
else
# :line
line_for_version(version, options)
end
output << version_rendering if options[:format] == :html
options[:top] += options[:top_increment]
# Remove the project requirement for Versions because it will
# restrict issues to only be on the current project. This
# ends up missing issues which are assigned to shared versions.
@query.project = nil if @query.project
issues = version.fixed_issues.for_gantt.with_query(@query)
if issues
# Indent issues
options[:indent] += options[:indent_increment]
output << render_issues(issues, options)
options[:indent] -= options[:indent_increment]
end
output
end
def subject_for_project(project, options)
case options[:format]
when :html
output = ''
output << "<div class='project-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
if project.is_a? Project
output << "<span class='icon icon-projects #{project.overdue? ? 'project-overdue' : ''}'>"
output << view.link_to_project(project)
output << '</span>'
else
ActiveRecord::Base.logger.debug "Gantt#subject_for_project was not given a project"
''
end
output << "</small></div>"
output
when :image
options[:image].fill('black')
options[:image].stroke('transparent')
options[:image].stroke_width(1)
options[:image].text(options[:indent], options[:top] + 2, project.name)
when :pdf
options[:pdf].SetY(options[:top])
options[:pdf].SetX(15)
char_limit = PDF::MaxCharactorsForSubject - options[:indent]
options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{project.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
options[:pdf].SetY(options[:top])
options[:pdf].SetX(options[:subject_width])
options[:pdf].Cell(options[:g_width], 5, "", "LR")
end
end
def line_for_project(project, options)
# Skip versions that don't have a start_date
if project.is_a?(Project) && project.start_date
options[:zoom] ||= 1
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
case options[:format]
when :html
output = ''
i_left = ((project.start_date - self.date_from)*options[:zoom]).floor
start_date = project.start_date
start_date ||= self.date_from
start_left = ((start_date - self.date_from)*options[:zoom]).floor
i_end_date = ((project.due_date <= self.date_to) ? project.due_date : self.date_to )
i_done_date = start_date + ((project.due_date - start_date+1)* project.completed_percent(:include_subprojects => true)/100).floor
i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if start_date < Date.today
i_end = ((i_end_date - self.date_from) * options[:zoom]).floor
i_width = (i_end - i_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
# Bar graphic
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_end > 0 && i_left <= options[:g_width]
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task project_todo'>&nbsp;</div>"
end
if l_width > 0 && i_left <= options[:g_width]
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task project_late'>&nbsp;</div>"
end
if d_width > 0 && i_left <= options[:g_width]
output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task project_done'>&nbsp;</div>"
end
# Starting diamond
if start_left <= options[:g_width] && start_left > 0
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task project-line starting'>&nbsp;</div>"
output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;' class='task label'>"
output << "</div>"
end
# Ending diamond
# Don't show items too far ahead
if i_end <= options[:g_width] && i_end > 0
output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task project-line ending'>&nbsp;</div>"
end
# DIsplay the Project name and %
if i_end <= options[:g_width]
# Display the status even if it's floated off to the left
status_px = i_end + 12 # 12px for the diamond
status_px = 0 if status_px <= 0
output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label project-name'>"
output << "<strong>#{h project } #{h project.completed_percent(:include_subprojects => true).to_i.to_s}%</strong>"
output << "</div>"
end
output
when :image
options[:image].stroke('transparent')
i_left = options[:subject_width] + ((project.due_date - self.date_from)*options[:zoom]).floor
# Make sure negative i_left doesn't overflow the subject
if i_left > options[:subject_width]
options[:image].fill('blue')
options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
options[:image].fill('black')
options[:image].text(i_left + 11, options[:top] + 1, project.name)
end
when :pdf
options[:pdf].SetY(options[:top]+1.5)
i_left = ((project.due_date - @date_from)*options[:zoom])
# Make sure negative i_left doesn't overflow the subject
if i_left > 0
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(50,50,200)
options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
options[:pdf].SetY(options[:top]+1.5)
options[:pdf].SetX(options[:subject_width] + i_left + 3)
options[:pdf].Cell(30, 2, "#{project.name}")
end
end
else
ActiveRecord::Base.logger.debug "Gantt#line_for_project was not given a project with a start_date"
''
end
end
def subject_for_version(version, options)
case options[:format]
when :html
output = ''
output << "<div class='version-name' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
if version.is_a? Version
output << "<span class='icon icon-package #{version.behind_schedule? ? 'version-behind-schedule' : ''} #{version.overdue? ? 'version-overdue' : ''}'>"
output << view.link_to_version(version)
output << '</span>'
else
ActiveRecord::Base.logger.debug "Gantt#subject_for_version was not given a version"
''
end
output << "</small></div>"
output
when :image
options[:image].fill('black')
options[:image].stroke('transparent')
options[:image].stroke_width(1)
options[:image].text(options[:indent], options[:top] + 2, version.name)
when :pdf
options[:pdf].SetY(options[:top])
options[:pdf].SetX(15)
char_limit = PDF::MaxCharactorsForSubject - options[:indent]
options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{version.name}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
options[:pdf].SetY(options[:top])
options[:pdf].SetX(options[:subject_width])
options[:pdf].Cell(options[:g_width], 5, "", "LR")
end
end
def line_for_version(version, options)
# Skip versions that don't have a start_date
if version.is_a?(Version) && version.start_date
options[:zoom] ||= 1
options[:g_width] ||= (self.date_to - self.date_from + 1) * options[:zoom]
case options[:format]
when :html
output = ''
i_left = ((version.start_date - self.date_from)*options[:zoom]).floor
# TODO: or version.fixed_issues.collect(&:start_date).min
start_date = version.fixed_issues.minimum('start_date') if version.fixed_issues.present?
start_date ||= self.date_from
start_left = ((start_date - self.date_from)*options[:zoom]).floor
i_end_date = ((version.due_date <= self.date_to) ? version.due_date : self.date_to )
i_done_date = start_date + ((version.due_date - start_date+1)* version.completed_pourcent/100).floor
i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if start_date < Date.today
i_width = (i_left - start_left + 1).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - start_date)*options[:zoom]).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
i_end = ((i_end_date - self.date_from) * options[:zoom]).floor # Ending pixel
# Bar graphic
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_width > 0 && i_left <= options[:g_width]
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ i_width }px;' class='task milestone_todo'>&nbsp;</div>"
end
if l_width > 0 && i_left <= options[:g_width]
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ l_width }px;' class='task milestone_late'>&nbsp;</div>"
end
if d_width > 0 && i_left <= options[:g_width]
output<< "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:#{ d_width }px;' class='task milestone_done'>&nbsp;</div>"
end
# Starting diamond
if start_left <= options[:g_width] && start_left > 0
output << "<div style='top:#{ options[:top] }px;left:#{ start_left }px;width:15px;' class='task milestone starting'>&nbsp;</div>"
output << "<div style='top:#{ options[:top] }px;left:#{ start_left + 12 }px;background:#fff;' class='task'>"
output << "</div>"
end
# Ending diamond
# Don't show items too far ahead
if i_left <= options[:g_width] && i_end > 0
output << "<div style='top:#{ options[:top] }px;left:#{ i_end }px;width:15px;' class='task milestone ending'>&nbsp;</div>"
end
# Display the Version name and %
if i_end <= options[:g_width]
# Display the status even if it's floated off to the left
status_px = i_end + 12 # 12px for the diamond
status_px = 0 if status_px <= 0
output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='task label version-name'>"
output << h("#{version.project} -") unless @project && @project == version.project
output << "<strong>#{h version } #{h version.completed_pourcent.to_i.to_s}%</strong>"
output << "</div>"
end
output
when :image
options[:image].stroke('transparent')
i_left = options[:subject_width] + ((version.start_date - @date_from)*options[:zoom]).floor
# Make sure negative i_left doesn't overflow the subject
if i_left > options[:subject_width]
options[:image].fill('green')
options[:image].rectangle(i_left, options[:top], i_left + 6, options[:top] - 6)
options[:image].fill('black')
options[:image].text(i_left + 11, options[:top] + 1, version.name)
end
when :pdf
options[:pdf].SetY(options[:top]+1.5)
i_left = ((version.start_date - @date_from)*options[:zoom])
# Make sure negative i_left doesn't overflow the subject
if i_left > 0
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(50,200,50)
options[:pdf].Cell(2, 2, "", 0, 0, "", 1)
options[:pdf].SetY(options[:top]+1.5)
options[:pdf].SetX(options[:subject_width] + i_left + 3)
options[:pdf].Cell(30, 2, "#{version.name}")
end
end
else
ActiveRecord::Base.logger.debug "Gantt#line_for_version was not given a version with a start_date"
''
end
end
def subject_for_issue(issue, options)
case options[:format]
when :html
output = ''
output << "<div class='tooltip'>"
output << "<div class='issue-subject' style='position: absolute;line-height:1.2em;height:16px;top:#{options[:top]}px;left:#{options[:indent]}px;overflow:hidden;'><small> "
if issue.is_a? Issue
css_classes = []
css_classes << 'issue-overdue' if issue.overdue?
css_classes << 'issue-behind-schedule' if issue.behind_schedule?
css_classes << 'icon icon-issue' unless Setting.gravatar_enabled? && issue.assigned_to
if issue.assigned_to.present?
assigned_string = l(:field_assigned_to) + ": " + issue.assigned_to.name
output << view.avatar(issue.assigned_to, :class => 'gravatar icon-gravatar', :size => 10, :title => assigned_string)
end
output << "<span class='#{css_classes.join(' ')}'>"
output << view.link_to_issue(issue)
output << ":"
output << h(issue.subject)
output << '</span>'
else
ActiveRecord::Base.logger.debug "Gantt#subject_for_issue was not given an issue"
''
end
output << "</small></div>"
# Tooltip
if issue.is_a? Issue
output << "<span class='tip' style='position: absolute;top:#{ options[:top].to_i + 16 }px;left:#{ options[:indent].to_i + 20 }px;'>"
output << view.render_issue_tooltip(issue)
output << "</span>"
end
output << "</div>"
output
when :image
options[:image].fill('black')
options[:image].stroke('transparent')
options[:image].stroke_width(1)
options[:image].text(options[:indent], options[:top] + 2, issue.subject)
when :pdf
options[:pdf].SetY(options[:top])
options[:pdf].SetX(15)
char_limit = PDF::MaxCharactorsForSubject - options[:indent]
options[:pdf].Cell(options[:subject_width]-15, 5, (" " * options[:indent]) +"#{issue.tracker} #{issue.id}: #{issue.subject}".sub(/^(.{#{char_limit}}[^\s]*\s).*$/, '\1 (...)'), "LR")
options[:pdf].SetY(options[:top])
options[:pdf].SetX(options[:subject_width])
options[:pdf].Cell(options[:g_width], 5, "", "LR")
end
end
def line_for_issue(issue, options)
# Skip issues that don't have a due_before (due_date or version's due_date)
if issue.is_a?(Issue) && issue.due_before
case options[:format]
when :html
output = ''
# Handle nil start_dates, rare but can happen.
i_start_date = if issue.start_date && issue.start_date >= self.date_from
issue.start_date
else
self.date_from
end
i_end_date = ((issue.due_before && issue.due_before <= self.date_to) ? issue.due_before : self.date_to )
i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
i_done_date = (i_done_date <= self.date_from ? self.date_from : i_done_date )
i_done_date = (i_done_date >= self.date_to ? self.date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - self.date_from)*options[:zoom]).floor
i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor - 2 # total width of the issue (- 2 for left and right borders)
d_width = ((i_done_date - i_start_date)*options[:zoom]).floor - 2 # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor - 2 : 0 # delay width
css = "task " + (issue.leaf? ? 'leaf' : 'parent')
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_width > 0
output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;' class='#{css} task_todo'>&nbsp;</div>"
end
if l_width > 0
output << "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ l_width }px;' class='#{css} task_late'>&nbsp;</div>"
end
if d_width > 0
output<< "<div style='top:#{ options[:top] }px;left:#{ i_left }px;width:#{ d_width }px;' class='#{css} task_done'>&nbsp;</div>"
end
# Display the status even if it's floated off to the left
status_px = i_left + i_width + 5
status_px = 5 if status_px <= 0
output << "<div style='top:#{ options[:top] }px;left:#{ status_px }px;' class='#{css} label issue-name'>"
output << issue.status.name
output << ' '
output << (issue.done_ratio).to_i.to_s
output << "%"
output << "</div>"
output << "<div class='tooltip' style='position: absolute;top:#{ options[:top] }px;left:#{ i_left }px;width:#{ i_width }px;height:12px;'>"
output << '<span class="tip">'
output << view.render_issue_tooltip(issue)
output << "</span></div>"
output
when :image
# Handle nil start_dates, rare but can happen.
i_start_date = if issue.start_date && issue.start_date >= @date_from
issue.start_date
else
@date_from
end
i_end_date = (issue.due_before <= date_to ? issue.due_before : date_to )
i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = options[:subject_width] + ((i_start_date - @date_from)*options[:zoom]).floor
i_width = ((i_end_date - i_start_date + 1)*options[:zoom]).floor # total width of the issue
d_width = ((i_done_date - i_start_date)*options[:zoom]).floor # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*options[:zoom]).floor : 0 # delay width
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_width > 0
options[:image].fill('grey')
options[:image].rectangle(i_left, options[:top], i_left + i_width, options[:top] - 6)
options[:image].fill('red')
options[:image].rectangle(i_left, options[:top], i_left + l_width, options[:top] - 6) if l_width > 0
options[:image].fill('blue')
options[:image].rectangle(i_left, options[:top], i_left + d_width, options[:top] - 6) if d_width > 0
end
# Show the status and % done next to the subject if it overflows
options[:image].fill('black')
if i_width > 0
options[:image].text(i_left + i_width + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
else
options[:image].text(options[:subject_width] + 5,options[:top] + 1, "#{issue.status.name} #{issue.done_ratio}%")
end
when :pdf
options[:pdf].SetY(options[:top]+1.5)
# Handle nil start_dates, rare but can happen.
i_start_date = if issue.start_date && issue.start_date >= @date_from
issue.start_date
else
@date_from
end
i_end_date = (issue.due_before <= @date_to ? issue.due_before : @date_to )
i_done_date = i_start_date + ((issue.due_before - i_start_date+1)*issue.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= @date_to ? @date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = ((i_start_date - @date_from)*options[:zoom])
i_width = ((i_end_date - i_start_date + 1)*options[:zoom])
d_width = ((i_done_date - i_start_date)*options[:zoom])
l_width = ((i_late_date - i_start_date+1)*options[:zoom]) if i_late_date
l_width ||= 0
# Make sure that negative i_left and i_width don't
# overflow the subject
if i_width > 0
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(200,200,200)
options[:pdf].Cell(i_width, 2, "", 0, 0, "", 1)
end
if l_width > 0
options[:pdf].SetY(options[:top]+1.5)
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(255,100,100)
options[:pdf].Cell(l_width, 2, "", 0, 0, "", 1)
end
if d_width > 0
options[:pdf].SetY(options[:top]+1.5)
options[:pdf].SetX(options[:subject_width] + i_left)
options[:pdf].SetFillColor(100,100,255)
options[:pdf].Cell(d_width, 2, "", 0, 0, "", 1)
end
options[:pdf].SetY(options[:top]+1.5)
# Make sure that negative i_left and i_width don't
# overflow the subject
if (i_left + i_width) >= 0
options[:pdf].SetX(options[:subject_width] + i_left + i_width)
else
options[:pdf].SetX(options[:subject_width])
end
options[:pdf].Cell(30, 2, "#{issue.status} #{issue.done_ratio}%")
end
else
ActiveRecord::Base.logger.debug "GanttHelper#line_for_issue was not given an issue with a due_before"
''
end
end
# Generates a gantt image
# Only defined if RMagick is avalaible
def to_image(project, format='PNG')
def to_image(format='PNG')
date_to = (@date_from >> @months)-1
show_weeks = @zoom > 1
show_days = @zoom > 2
@ -101,7 +735,7 @@ module Redmine
# width of one day in pixels
zoom = @zoom*2
g_width = (@date_to - @date_from + 1)*zoom
g_height = 20 * events.length + 20
g_height = 20 * number_of_rows + 30
headers_heigth = (show_weeks ? 2*header_heigth : header_heigth)
height = g_height + headers_heigth
@ -110,21 +744,7 @@ module Redmine
gc = Magick::Draw.new
# Subjects
top = headers_heigth + 20
gc.fill('black')
gc.stroke('transparent')
gc.stroke_width(1)
events.each do |i|
text = ""
if i.is_a? Issue
text = "#{i.tracker} #{i.id}: #{i.subject}"
else
text = i.name
end
text = "#{i.project} - #{text}" unless project && project == i.project
gc.text(4, top + 2, text)
top = top + 20
end
subjects(:image => gc, :top => (headers_heigth + 20), :indent => 4, :format => :image)
# Months headers
month_f = @date_from
@ -202,38 +822,8 @@ module Redmine
# content
top = headers_heigth + 20
gc.stroke('transparent')
events.each do |i|
if i.is_a?(Issue)
i_start_date = (i.start_date >= @date_from ? i.start_date : @date_from )
i_end_date = (i.due_before <= date_to ? i.due_before : date_to )
i_done_date = i.start_date + ((i.due_before - i.start_date+1)*i.done_ratio/100).floor
i_done_date = (i_done_date <= @date_from ? @date_from : i_done_date )
i_done_date = (i_done_date >= date_to ? date_to : i_done_date )
i_late_date = [i_end_date, Date.today].min if i_start_date < Date.today
i_left = subject_width + ((i_start_date - @date_from)*zoom).floor
i_width = ((i_end_date - i_start_date + 1)*zoom).floor # total width of the issue
d_width = ((i_done_date - i_start_date)*zoom).floor # done width
l_width = i_late_date ? ((i_late_date - i_start_date+1)*zoom).floor : 0 # delay width
gc.fill('grey')
gc.rectangle(i_left, top, i_left + i_width, top - 6)
gc.fill('red')
gc.rectangle(i_left, top, i_left + l_width, top - 6) if l_width > 0
gc.fill('blue')
gc.rectangle(i_left, top, i_left + d_width, top - 6) if d_width > 0
gc.fill('black')
gc.text(i_left + i_width + 5,top + 1, "#{i.status.name} #{i.done_ratio}%")
else
i_left = subject_width + ((i.start_date - @date_from)*zoom).floor
gc.fill('green')
gc.rectangle(i_left, top, i_left + 6, top - 6)
gc.fill('black')
gc.text(i_left + 11, top + 1, i.name)
end
top = top + 20
end
lines(:image => gc, :top => top, :zoom => zoom, :subject_width => subject_width, :format => :image)
# today red line
if Date.today >= @date_from and Date.today <= date_to
@ -247,35 +837,136 @@ module Redmine
imgl.to_blob
end if Object.const_defined?(:Magick)
def to_pdf
pdf = ::Redmine::Export::PDF::IFPDF.new(current_language)
pdf.SetTitle("#{l(:label_gantt)} #{project}")
pdf.AliasNbPages
pdf.footer_date = format_date(Date.today)
pdf.AddPage("L")
pdf.SetFontStyle('B',12)
pdf.SetX(15)
pdf.Cell(PDF::LeftPaneWidth, 20, project.to_s)
pdf.Ln
pdf.SetFontStyle('B',9)
subject_width = PDF::LeftPaneWidth
header_heigth = 5
headers_heigth = header_heigth
show_weeks = false
show_days = false
if self.months < 7
show_weeks = true
headers_heigth = 2*header_heigth
if self.months < 3
show_days = true
headers_heigth = 3*header_heigth
end
end
g_width = PDF.right_pane_width
zoom = (g_width) / (self.date_to - self.date_from + 1)
g_height = 120
t_height = g_height + headers_heigth
y_start = pdf.GetY
# Months headers
month_f = self.date_from
left = subject_width
height = header_heigth
self.months.times do
width = ((month_f >> 1) - month_f) * zoom
pdf.SetY(y_start)
pdf.SetX(left)
pdf.Cell(width, height, "#{month_f.year}-#{month_f.month}", "LTR", 0, "C")
left = left + width
month_f = month_f >> 1
end
# Weeks headers
if show_weeks
left = subject_width
height = header_heigth
if self.date_from.cwday == 1
# self.date_from is monday
week_f = self.date_from
else
# find next monday after self.date_from
week_f = self.date_from + (7 - self.date_from.cwday + 1)
width = (7 - self.date_from.cwday + 1) * zoom-1
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width + 1, height, "", "LTR")
left = left + width+1
end
while week_f <= self.date_to
width = (week_f + 6 <= self.date_to) ? 7 * zoom : (self.date_to - week_f + 1) * zoom
pdf.SetY(y_start + header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, (width >= 5 ? week_f.cweek.to_s : ""), "LTR", 0, "C")
left = left + width
week_f = week_f+7
end
end
# Days headers
if show_days
left = subject_width
height = header_heigth
wday = self.date_from.cwday
pdf.SetFontStyle('B',7)
(self.date_to - self.date_from + 1).to_i.times do
width = zoom
pdf.SetY(y_start + 2 * header_heigth)
pdf.SetX(left)
pdf.Cell(width, height, day_name(wday).first, "LTR", 0, "C")
left = left + width
wday = wday + 1
wday = 1 if wday > 7
end
end
pdf.SetY(y_start)
pdf.SetX(15)
pdf.Cell(subject_width+g_width-15, headers_heigth, "", 1)
# Tasks
top = headers_heigth + y_start
pdf_subjects_and_lines(pdf, {
:top => top,
:zoom => zoom,
:subject_width => subject_width,
:g_width => g_width
})
pdf.Line(15, top, subject_width+g_width, top)
pdf.Output
end
private
def gantt_issue_compare(x, y, issues)
if x.parent_id == y.parent_id
gantt_start_compare(x, y)
elsif x.is_ancestor_of?(y)
-1
elsif y.is_ancestor_of?(x)
1
# Renders both the subjects and lines of the Gantt chart for the
# PDF format
def pdf_subjects_and_lines(pdf, options = {})
subject_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :subject, :format => :pdf, :pdf => pdf}.merge(options)
line_options = {:indent => 0, :indent_increment => 5, :top_increment => 3, :render => :line, :format => :pdf, :pdf => pdf}.merge(options)
if @project
render_project(@project, subject_options)
render_project(@project, line_options)
else
ax = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(x) && !i.is_ancestor_of?(y) }.sort_by(&:lft).first
ay = issues.select {|i| i.is_a?(Issue) && i.is_ancestor_of?(y) && !i.is_ancestor_of?(x) }.sort_by(&:lft).first
if ax.nil? && ay.nil?
gantt_start_compare(x, y)
else
gantt_issue_compare(ax || x, ay || y, issues)
Project.roots.each do |project|
render_project(project, subject_options)
render_project(project, line_options)
end
end
end
def gantt_start_compare(x, y)
if x.start_date.nil?
-1
elsif y.start_date.nil?
1
else
x.start_date <=> y.start_date
end
end
end
end
end

Binary file not shown.

Before

Width:  |  Height:  |  Size: 122 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 155 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 204 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 855 B

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 B

View File

@ -787,8 +787,10 @@ background-image:url('../images/close_hl.png');
white-space:nowrap;
}
.task.label {width:100%;}
.task_late { background:#f66 url(../images/task_late.png); border: 1px solid #f66; }
.task_done { background:#66f url(../images/task_done.png); border: 1px solid #66f; }
.task_done { background:#00c600 url(../images/task_done.png); border: 1px solid #00c600; }
.task_todo { background:#aaa url(../images/task_todo.png); border: 1px solid #aaa; }
.task_todo.parent { background: #888; border: 1px solid #888; height: 6px;}
@ -796,7 +798,17 @@ background-image:url('../images/close_hl.png');
.task_todo.parent .left { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-left: -5px; left: 0px; top: -1px;}
.task_todo.parent .right { position: absolute; background: url(../images/task_parent_end.png) no-repeat 0 0; width: 8px; height: 16px; margin-right: -5px; right: 0px; top: -1px;}
.milestone { background-image:url(../images/milestone.png); background-repeat: no-repeat; border: 0; }
.milestone { background-image:url(../images/version_marker.png); background-repeat: no-repeat; border: 0; }
.milestone_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
.milestone_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
.milestone_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
.project-line { background-image:url(../images/project_marker.png); background-repeat: no-repeat; border: 0; }
.project_late { background:#f66 url(../images/milestone_late.png); border: 1px solid #f66; height: 2px; margin-top: 3px;}
.project_done { background:#00c600 url(../images/milestone_done.png); border: 1px solid #00c600; height: 2px; margin-top: 3px;}
.project_todo { background:#fff url(../images/milestone_todo.png); border: 1px solid #fff; height: 2px; margin-top: 3px;}
.version-behind-schedule a, .issue-behind-schedule a {color: #f66914;}
.version-overdue a, .issue-overdue a, .project-overdue a {color: #f00;}
/***** Icons *****/
.icon {
@ -840,6 +852,7 @@ padding-bottom: 3px;
.icon-comment { background-image: url(../images/comment.png); }
.icon-summary { background-image: url(../images/lightning.png); }
.icon-server-authentication { background-image: url(../images/server_key.png); }
.icon-issue { background-image: url(../images/ticket.png); }
.icon-file { background-image: url(../images/files/default.png); }
.icon-file.text-plain { background-image: url(../images/files/text.png); }
@ -898,6 +911,12 @@ td.username img.gravatar {
margin: 0 1em 1em 0;
}
/* Used on 12px Gravatar img tags without the icon background */
.icon-gravatar {
float: left;
margin-right: 4px;
}
#activity dt,
.journal {
clear: left;

View File

@ -5,20 +5,20 @@ class GanttsControllerTest < ActionController::TestCase
context "#gantt" do
should "work" do
i2 = Issue.find(2)
i2.update_attribute(:due_date, 1.month.from_now)
get :show, :project_id => 1
assert_response :success
assert_template 'show.html.erb'
assert_not_nil assigns(:gantt)
events = assigns(:gantt).events
assert_not_nil events
# Issue with start and due dates
i = Issue.find(1)
assert_not_nil i.due_date
assert events.include?(Issue.find(1))
# Issue with without due date but targeted to a version with date
assert_select "div a.issue", /##{i.id}/
# Issue with on a targeted version should not be in the events but loaded in the html
i = Issue.find(2)
assert_nil i.due_date
assert events.include?(i)
assert_select "div a.issue", /##{i.id}/
end
should "work cross project" do
@ -26,8 +26,8 @@ class GanttsControllerTest < ActionController::TestCase
assert_response :success
assert_template 'show.html.erb'
assert_not_nil assigns(:gantt)
events = assigns(:gantt).events
assert_not_nil events
assert_not_nil assigns(:gantt).query
assert_nil assigns(:gantt).project
end
should "export to pdf" do

View File

@ -25,8 +25,9 @@ module ObjectDaddyHelpers
def Issue.generate_for_project!(project, attributes={})
issue = Issue.spawn(attributes) do |issue|
issue.project = project
issue.tracker = project.trackers.first unless project.trackers.empty?
yield issue if block_given?
end
issue.tracker = project.trackers.first unless project.trackers.empty?
issue.save!
issue
end

View File

@ -601,7 +601,7 @@ EXPECTED
# turn off avatars
Setting.gravatar_enabled = '0'
assert_nil avatar(User.find_by_mail('jsmith@somenet.foo'))
assert_equal '', avatar(User.find_by_mail('jsmith@somenet.foo'))
end
def test_link_to_user

View File

@ -511,6 +511,28 @@ class IssueTest < ActiveSupport::TestCase
assert !Issue.new(:due_date => 1.day.ago.to_date, :status => IssueStatus.find(:first, :conditions => {:is_closed => true})).overdue?
end
context "#behind_schedule?" do
should "be false if the issue has no start_date" do
assert !Issue.new(:start_date => nil, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
end
should "be false if the issue has no end_date" do
assert !Issue.new(:start_date => 1.day.from_now.to_date, :due_date => nil, :done_ratio => 0).behind_schedule?
end
should "be false if the issue has more done than it's calendar time" do
assert !Issue.new(:start_date => 50.days.ago.to_date, :due_date => 50.days.from_now.to_date, :done_ratio => 90).behind_schedule?
end
should "be true if the issue hasn't been started at all" do
assert Issue.new(:start_date => 1.day.ago.to_date, :due_date => 1.day.from_now.to_date, :done_ratio => 0).behind_schedule?
end
should "be true if the issue has used more calendar time than it's done ratio" do
assert Issue.new(:start_date => 100.days.ago.to_date, :due_date => Date.today, :done_ratio => 90).behind_schedule?
end
end
def test_assignable_users
assert_kind_of User, Issue.find(1).assignable_users.first
end

View File

@ -0,0 +1,703 @@
# redMine - project management software
# Copyright (C) 2006-2008 Jean-Philippe Lang
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
require File.dirname(__FILE__) + '/../../../../test_helper'
class Redmine::Helpers::GanttTest < ActiveSupport::TestCase
# Utility methods and classes so assert_select can be used.
class GanttViewTest < ActionView::Base
include ActionView::Helpers::UrlHelper
include ActionView::Helpers::TextHelper
include ActionController::UrlWriter
include ApplicationHelper
include ProjectsHelper
include IssuesHelper
def self.default_url_options
{:only_path => true }
end
end
include ActionController::Assertions::SelectorAssertions
def setup
@response = ActionController::TestResponse.new
# Fixtures
ProjectCustomField.delete_all
Project.destroy_all
User.current = User.find(1)
end
def build_view
@view = GanttViewTest.new
end
def html_document
HTML::Document.new(@response.body)
end
# Creates a Gantt chart for a 4 week span
def create_gantt(project=Project.generate!)
@project = project
@gantt = Redmine::Helpers::Gantt.new
@gantt.project = @project
@gantt.query = Query.generate_default!(:project => @project)
@gantt.view = build_view
@gantt.instance_variable_set('@date_from', 2.weeks.ago.to_date)
@gantt.instance_variable_set('@date_to', 2.weeks.from_now.to_date)
end
context "#number_of_rows" do
context "with one project" do
should "return the number of rows just for that project"
end
context "with no project" do
should "return the total number of rows for all the projects, resursively"
end
end
context "#number_of_rows_on_project" do
setup do
create_gantt
end
should "clear the @query.project so cross-project issues and versions can be counted" do
assert @gantt.query.project
@gantt.number_of_rows_on_project(@project)
assert_nil @gantt.query.project
end
should "count 1 for the project itself" do
assert_equal 1, @gantt.number_of_rows_on_project(@project)
end
should "count the number of issues without a version" do
@project.issues << Issue.generate_for_project!(@project, :fixed_version => nil)
assert_equal 2, @gantt.number_of_rows_on_project(@project)
end
should "count the number of versions" do
@project.versions << Version.generate!
@project.versions << Version.generate!
assert_equal 3, @gantt.number_of_rows_on_project(@project)
end
should "count the number of issues on versions, including cross-project" do
version = Version.generate!
@project.versions << version
@project.issues << Issue.generate_for_project!(@project, :fixed_version => version)
assert_equal 3, @gantt.number_of_rows_on_project(@project)
end
should "recursive and count the number of rows on each subproject" do
@project.versions << Version.generate! # +1
@subproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1
@subproject.set_parent!(@project)
@subproject.issues << Issue.generate_for_project!(@subproject) # +1
@subproject.issues << Issue.generate_for_project!(@subproject) # +1
@subsubproject = Project.generate!(:enabled_module_names => ['issue_tracking']) # +1
@subsubproject.set_parent!(@subproject)
@subsubproject.issues << Issue.generate_for_project!(@subsubproject) # +1
assert_equal 7, @gantt.number_of_rows_on_project(@project) # +1 for self
end
end
# TODO: more of an integration test
context "#subjects" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => 1.week.from_now.to_date, :sharing => 'none')
@project.versions << @version
@issue = Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
@project.issues << @issue
@response.body = @gantt.subjects
end
context "project" do
should "be rendered" do
assert_select "div.project-name a", /#{@project.name}/
end
should "have an indent of 4" do
assert_select "div.project-name[style*=left:4px]"
end
end
context "version" do
should "be rendered" do
assert_select "div.version-name a", /#{@version.name}/
end
should "be indented 24 (one level)" do
assert_select "div.version-name[style*=left:24px]"
end
end
context "issue" do
should "be rendered" do
assert_select "div.issue-subject", /#{@issue.subject}/
end
should "be indented 44 (two levels)" do
assert_select "div.issue-subject[style*=left:44px]"
end
end
end
context "#lines" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => 1.week.from_now.to_date)
@project.versions << @version
@issue = Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
@project.issues << @issue
@response.body = @gantt.lines
end
context "project" do
should "be rendered" do
assert_select "div.project_todo"
assert_select "div.project-line.starting"
assert_select "div.project-line.ending"
assert_select "div.label.project-name", /#{@project.name}/
end
end
context "version" do
should "be rendered" do
assert_select "div.milestone_todo"
assert_select "div.milestone.starting"
assert_select "div.milestone.ending"
assert_select "div.label.version-name", /#{@version.name}/
end
end
context "issue" do
should "be rendered" do
assert_select "div.task_todo"
assert_select "div.label.issue-name", /#{@issue.done_ratio}/
assert_select "div.tooltip", /#{@issue.subject}/
end
end
end
context "#render_project" do
should "be tested"
end
context "#render_issues" do
should "be tested"
end
context "#render_version" do
should "be tested"
end
context "#subject_for_project" do
setup do
create_gantt
end
context ":html format" do
should "add an absolute positioned div" do
@response.body = @gantt.subject_for_project(@project, {:format => :html})
assert_select "div[style*=absolute]"
end
should "use the indent option to move the div to the right" do
@response.body = @gantt.subject_for_project(@project, {:format => :html, :indent => 40})
assert_select "div[style*=left:40]"
end
should "include the project name" do
@response.body = @gantt.subject_for_project(@project, {:format => :html})
assert_select 'div', :text => /#{@project.name}/
end
should "include a link to the project" do
@response.body = @gantt.subject_for_project(@project, {:format => :html})
assert_select 'a[href=?]', "/projects/#{@project.identifier}", :text => /#{@project.name}/
end
should "style overdue projects" do
@project.enabled_module_names = [:issue_tracking]
@project.versions << Version.generate!(:effective_date => Date.yesterday)
assert @project.overdue?, "Need an overdue project for this test"
@response.body = @gantt.subject_for_project(@project, {:format => :html})
assert_select 'div span.project-overdue'
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#line_for_project" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => Date.yesterday)
@project.versions << @version
@project.issues << Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
end
context ":html format" do
context "todo line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_todo[style*=left:52px]"
end
should "be the total width of the project" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_todo[style*=width:31px]"
end
end
context "late line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_late[style*=left:52px]"
end
should "be the total delayed width of the project" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_late[style*=width:6px]"
end
end
context "done line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_done[style*=left:52px]"
end
should "Be the total done width of the project" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project_done[style*=left:52px]"
end
end
context "starting marker" do
should "not appear if the starting point is off the gantt chart" do
# Shift the date range of the chart
@gantt.instance_variable_set('@date_from', Date.today)
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-line.starting", false
end
should "appear at the starting point" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-line.starting[style*=left:52px]"
end
end
context "ending marker" do
should "not appear if the starting point is off the gantt chart" do
# Shift the date range of the chart
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-line.ending", false
end
should "appear at the end of the date range" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-line.ending[style*=left:84px]"
end
end
context "status content" do
should "appear at the far left, even if it's far in the past" do
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-name", /#{@project.name}/
end
should "show the project name" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-name", /#{@project.name}/
end
should "show the percent complete" do
@response.body = @gantt.line_for_project(@project, {:format => :html, :zoom => 4})
assert_select "div.project-name", /0%/
end
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#subject_for_version" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => Date.yesterday)
@project.versions << @version
@project.issues << Issue.generate!(:fixed_version => @version,
:subject => "gantt#subject_for_version",
:tracker => @tracker,
:project => @project,
:start_date => Date.today)
end
context ":html format" do
should "add an absolute positioned div" do
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select "div[style*=absolute]"
end
should "use the indent option to move the div to the right" do
@response.body = @gantt.subject_for_version(@version, {:format => :html, :indent => 40})
assert_select "div[style*=left:40]"
end
should "include the version name" do
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select 'div', :text => /#{@version.name}/
end
should "include a link to the version" do
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select 'a[href=?]', Regexp.escape("/versions/show/#{@version.to_param}"), :text => /#{@version.name}/
end
should "style late versions" do
assert @version.overdue?, "Need an overdue version for this test"
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select 'div span.version-behind-schedule'
end
should "style behind schedule versions" do
assert @version.behind_schedule?, "Need a behind schedule version for this test"
@response.body = @gantt.subject_for_version(@version, {:format => :html})
assert_select 'div span.version-behind-schedule'
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#line_for_version" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => 1.week.from_now.to_date)
@project.versions << @version
@project.issues << Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
end
context ":html format" do
context "todo line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_todo[style*=left:52px]"
end
should "be the total width of the version" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_todo[style*=width:31px]"
end
end
context "late line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_late[style*=left:52px]"
end
should "be the total delayed width of the version" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_late[style*=width:6px]"
end
end
context "done line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_done[style*=left:52px]"
end
should "Be the total done width of the version" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone_done[style*=left:52px]"
end
end
context "starting marker" do
should "not appear if the starting point is off the gantt chart" do
# Shift the date range of the chart
@gantt.instance_variable_set('@date_from', Date.today)
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone.starting", false
end
should "appear at the starting point" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone.starting[style*=left:52px]"
end
end
context "ending marker" do
should "not appear if the starting point is off the gantt chart" do
# Shift the date range of the chart
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone.ending", false
end
should "appear at the end of the date range" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.milestone.ending[style*=left:84px]"
end
end
context "status content" do
should "appear at the far left, even if it's far in the past" do
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.version-name", /#{@version.name}/
end
should "show the version name" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.version-name", /#{@version.name}/
end
should "show the percent complete" do
@response.body = @gantt.line_for_version(@version, {:format => :html, :zoom => 4})
assert_select "div.version-name", /30%/
end
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#subject_for_issue" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@issue = Issue.generate!(:subject => "gantt#subject_for_issue",
:tracker => @tracker,
:project => @project,
:start_date => 3.days.ago.to_date,
:due_date => Date.yesterday)
@project.issues << @issue
end
context ":html format" do
should "add an absolute positioned div" do
@response.body = @gantt.subject_for_issue(@issue, {:format => :html})
assert_select "div[style*=absolute]"
end
should "use the indent option to move the div to the right" do
@response.body = @gantt.subject_for_issue(@issue, {:format => :html, :indent => 40})
assert_select "div[style*=left:40]"
end
should "include the issue subject" do
@response.body = @gantt.subject_for_issue(@issue, {:format => :html})
assert_select 'div', :text => /#{@issue.subject}/
end
should "include a link to the issue" do
@response.body = @gantt.subject_for_issue(@issue, {:format => :html})
assert_select 'a[href=?]', Regexp.escape("/issues/#{@issue.to_param}"), :text => /#{@tracker.name} ##{@issue.id}/
end
should "style overdue issues" do
assert @issue.overdue?, "Need an overdue issue for this test"
@response.body = @gantt.subject_for_issue(@issue, {:format => :html})
assert_select 'div span.issue-overdue'
end
end
should "test the PNG format"
should "test the PDF format"
end
context "#line_for_issue" do
setup do
create_gantt
@project.enabled_module_names = [:issue_tracking]
@tracker = Tracker.generate!
@project.trackers << @tracker
@version = Version.generate!(:effective_date => 1.week.from_now.to_date)
@project.versions << @version
@issue = Issue.generate!(:fixed_version => @version,
:subject => "gantt#line_for_project",
:tracker => @tracker,
:project => @project,
:done_ratio => 30,
:start_date => Date.yesterday,
:due_date => 1.week.from_now.to_date)
@project.issues << @issue
end
context ":html format" do
context "todo line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_todo[style*=left:52px]"
end
should "be the total width of the issue" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_todo[style*=width:34px]"
end
end
context "late line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_late[style*=left:52px]"
end
should "be the total delayed width of the issue" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_late[style*=width:6px]"
end
end
context "done line" do
should "start from the starting point on the left" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_done[style*=left:52px]"
end
should "Be the total done width of the issue" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.task_done[style*=left:52px]"
end
end
context "status content" do
should "appear at the far left, even if it's far in the past" do
@gantt.instance_variable_set('@date_to', 2.weeks.ago.to_date)
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.issue-name"
end
should "show the issue status" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.issue-name", /#{@issue.status.name}/
end
should "show the percent complete" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.issue-name", /30%/
end
end
end
should "have an issue tooltip" do
@response.body = @gantt.line_for_issue(@issue, {:format => :html, :zoom => 4})
assert_select "div.tooltip", /#{@issue.subject}/
end
should "test the PNG format"
should "test the PDF format"
end
context "#to_image" do
should "be tested"
end
context "#to_pdf" do
should "be tested"
end
end

View File

@ -842,4 +842,122 @@ class ProjectTest < ActiveSupport::TestCase
end
context "#start_date" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
@project = Project.generate!(:identifier => 'test0')
@project.trackers << Tracker.generate!
end
should "be nil if there are no issues on the project" do
assert_nil @project.start_date
end
should "be nil if issue tracking is disabled" do
Issue.generate_for_project!(@project, :start_date => Date.today)
@project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
@project.reload
assert_nil @project.start_date
end
should "be the earliest start date of it's issues" do
early = 7.days.ago.to_date
Issue.generate_for_project!(@project, :start_date => Date.today)
Issue.generate_for_project!(@project, :start_date => early)
assert_equal early, @project.start_date
end
end
context "#due_date" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
@project = Project.generate!(:identifier => 'test0')
@project.trackers << Tracker.generate!
end
should "be nil if there are no issues on the project" do
assert_nil @project.due_date
end
should "be nil if issue tracking is disabled" do
Issue.generate_for_project!(@project, :due_date => Date.today)
@project.enabled_modules.find_all_by_name('issue_tracking').each {|m| m.destroy}
@project.reload
assert_nil @project.due_date
end
should "be the latest due date of it's issues" do
future = 7.days.from_now.to_date
Issue.generate_for_project!(@project, :due_date => future)
Issue.generate_for_project!(@project, :due_date => Date.today)
assert_equal future, @project.due_date
end
should "be the latest due date of it's versions" do
future = 7.days.from_now.to_date
@project.versions << Version.generate!(:effective_date => future)
@project.versions << Version.generate!(:effective_date => Date.today)
assert_equal future, @project.due_date
end
should "pick the latest date from it's issues and versions" do
future = 7.days.from_now.to_date
far_future = 14.days.from_now.to_date
Issue.generate_for_project!(@project, :due_date => far_future)
@project.versions << Version.generate!(:effective_date => future)
assert_equal far_future, @project.due_date
end
end
context "Project#completed_percent" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
@project = Project.generate!(:identifier => 'test0')
@project.trackers << Tracker.generate!
end
context "no versions" do
should "be 100" do
assert_equal 100, @project.completed_percent
end
end
context "with versions" do
should "return 0 if the versions have no issues" do
Version.generate!(:project => @project)
Version.generate!(:project => @project)
assert_equal 0, @project.completed_percent
end
should "return 100 if the version has only closed issues" do
v1 = Version.generate!(:project => @project)
Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v1)
v2 = Version.generate!(:project => @project)
Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('Closed'), :fixed_version => v2)
assert_equal 100, @project.completed_percent
end
should "return the averaged completed percent of the versions (not weighted)" do
v1 = Version.generate!(:project => @project)
Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v1)
v2 = Version.generate!(:project => @project)
Issue.generate_for_project!(@project, :status => IssueStatus.find_by_name('New'), :estimated_hours => 10, :done_ratio => 50, :fixed_version => v2)
assert_equal 50, @project.completed_percent
end
end
end
end

View File

@ -105,6 +105,56 @@ class VersionTest < ActiveSupport::TestCase
assert_progress_equal 25.0/100.0*100, v.closed_pourcent
end
context "#behind_schedule?" do
setup do
ProjectCustomField.destroy_all # Custom values are a mess to isolate in tests
@project = Project.generate!(:identifier => 'test0')
@project.trackers << Tracker.generate!
@version = Version.generate!(:project => @project, :effective_date => nil)
end
should "be false if there are no issues assigned" do
@version.update_attribute(:effective_date, Date.yesterday)
assert_equal false, @version.behind_schedule?
end
should "be false if there is no effective_date" do
assert_equal false, @version.behind_schedule?
end
should "be false if all of the issues are ahead of schedule" do
@version.update_attribute(:effective_date, 7.days.from_now.to_date)
@version.fixed_issues = [
Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60), # 14 day span, 60% done, 50% time left
Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60) # 14 day span, 60% done, 50% time left
]
assert_equal 60, @version.completed_pourcent
assert_equal false, @version.behind_schedule?
end
should "be true if any of the issues are behind schedule" do
@version.update_attribute(:effective_date, 7.days.from_now.to_date)
@version.fixed_issues = [
Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 60), # 14 day span, 60% done, 50% time left
Issue.generate_for_project!(@project, :start_date => 7.days.ago, :done_ratio => 20) # 14 day span, 20% done, 50% time left
]
assert_equal 40, @version.completed_pourcent
assert_equal true, @version.behind_schedule?
end
should "be false if all of the issues are complete" do
@version.update_attribute(:effective_date, 7.days.from_now.to_date)
@version.fixed_issues = [
Issue.generate_for_project!(@project, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)), # 7 day span
Issue.generate_for_project!(@project, :start_date => 14.days.ago, :done_ratio => 100, :status => IssueStatus.find(5)) # 7 day span
]
assert_equal 100, @version.completed_pourcent
assert_equal false, @version.behind_schedule?
end
end
context "#estimated_hours" do
setup do
@version = Version.create!(:project_id => 1, :name => '#estimated_hours')

View File

@ -6,7 +6,7 @@ task :default => :spec
desc 'Run all application-specific specs'
Spec::Rake::SpecTask.new(:spec) do |t|
t.rcov = true
# t.rcov = true
end
desc "Report code statistics (KLOCs, etc) from the application"

View File

@ -27,6 +27,9 @@ module GravatarHelper
# XHTML specs.
:alt => '',
# The title text to use for the img tag for the gravatar.
:title => '',
# The class to assign to the img tag for the gravatar.
:class => 'gravatar',
@ -48,8 +51,8 @@ module GravatarHelper
def gravatar(email, options={})
src = h(gravatar_url(email, options))
options = DEFAULT_OPTIONS.merge(options)
[:class, :alt, :size].each { |opt| options[opt] = h(options[opt]) }
"<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
[:class, :alt, :size, :title].each { |opt| options[opt] = h(options[opt]) }
"<img class=\"#{options[:class]}\" alt=\"#{options[:alt]}\" title=\"#{options[:title]}\" width=\"#{options[:size]}\" height=\"#{options[:size]}\" src=\"#{src}\" />"
end
# Returns the base Gravatar URL for the given email hash. If ssl evaluates to true,

View File

@ -4,34 +4,40 @@ require 'active_support' # to get "returning"
require File.dirname(__FILE__) + '/../lib/gravatar'
include GravatarHelper, GravatarHelper::PublicMethods, ERB::Util
context "gravatar_url with a custom default URL" do
setup do
describe "gravatar_url with a custom default URL" do
before(:each) do
@original_options = DEFAULT_OPTIONS.dup
DEFAULT_OPTIONS[:default] = "no_avatar.png"
@url = gravatar_url("somewhere")
end
specify "should include the \"default\" argument in the result" do
it "should include the \"default\" argument in the result" do
@url.should match(/&default=no_avatar.png/)
end
teardown do
after(:each) do
DEFAULT_OPTIONS.merge!(@original_options)
end
end
context "gravatar_url with default settings" do
setup do
describe "gravatar_url with default settings" do
before(:each) do
@url = gravatar_url("somewhere")
end
specify "should have a nil default URL" do
it "should have a nil default URL" do
DEFAULT_OPTIONS[:default].should be_nil
end
specify "should not include the \"default\" argument in the result" do
it "should not include the \"default\" argument in the result" do
@url.should_not match(/&default=/)
end
end
describe "gravatar with a custom title option" do
it "should include the title in the result" do
gravatar('example@example.com', :title => "This is a title attribute").should match(/This is a title attribute/)
end
end